Python 强类型编程
Python 强类型编程 python的dict类型是我们在开发中很经常使用的,很方便,可以直接对字段寻址
ret = json.load('{"field1": "value"}') ptrint(ret["field1"]) 但是这种编码方式也带来了一些弊端:
由于没有定义数据结构,会导致我们只能查看到数据源头来判断数据的构成,维护成本高 对于一些可能为空的字段,都需要单独的编码处理,太过繁琐,导致代码不够强壮 经过一番调查,发现 python 要使用强类型开发,最大的一个卡点就是json的序列化和反序列化,这里记录下调查结果。
如何定义数据结构 最常见的方式就是通过构造函数
class User: def __init___(self, name: str, age: int): self.name = name self.age = age 但是这个方式显而易见有个问题——由于字段定义和构造函数强绑定,导致字段的增加也会需要修改构造函数的入参和赋值逻辑,很繁琐。 所以python支持一个特性——dataclass,它会自动的生成构造函数和默认值,更贴近其他强类型语言的使用方式。
from dataclasses import dataclass @dataclass class User: name: str age:int = 18 如此一来,我们不再需要频繁调整构造函数和逻辑,非常棒。 在使用过程中,又发现了另一个问题——继承。 我们在User中定义了一些可选参数(有默认值的)和一些必选参数(无默认值的),这在没有继承的情况是没有问题的,但是一旦继承之后,会出现以下错误。
from dataclasses import dataclass @dataclass class User: name: str age:int = 18 @dataclass class ChildUser(User): address:str account: str = "000000" 错误:TypeError: non-default argument 'account' follows default argument 其原因是因为在继承后,可选参数与必选参数的位置顺序没有得到很好的处理,我们需要加上一个选项kw_only=True
Python-并发小记
Python-并发小记 这里记录一些最佳实践,避免踩坑
如果使用 async/await,尽量保持项目统一 由于 thread 与 coroutine 的执行方式差异较大,在项目中混用时可能会出现问题,比如 coroutine 没有得到执行,以及 coroutine 的线程安全性 如果由于历史原因导致必须新技术混用,请确保好以下几点:
同步阻塞函数可以使用 eventloop.run_in_executor 来包装为异步 同步阻塞函数如果想要在其他事件循环(或者说是线程,因为默认是执行在运行线程上)上运行一个新的异步函数,使用 asyncio.run_coroutine_threadsafe 在进入 async/await 语法中后,请确保后续语法不要再混入同步函数 使用 asyncio.run 而避免直接去操作事件循环 在同步调用中使用 asyncio时尽量新起一个线程来run,不然可能会在上游出现已经运行中的事件循环从而导致冲突 如果项目有coroutine的代码,避免使用 threading.local 这是因为 threading.local 并不是协程安全的,请使用 contextvars.ContextVar('var', default="default") 来替代,否则可能出现变量的竞写导致预期外的情况
生成新的 thread 要注意 thread.local 与 context var 的传播 由于python不像是.Net 那样,默认会对子线程进行传播,因此在创建新线程or线程池时要注意 thread.local 和 context var 的显式拷贝(进程也一样)
尽量使用 asyncio 封装函数,不要直接接触evetloop 这是因为底层的机制比较复杂,比如对于 main thread 会默认初始化一个事件循环,但是新创建的子线程却不会。
注意避免使用同步IO 比如老牌的 requests 库,底层是基于 urllib3 的,很遗憾,这是个同步的 io 库,它会导致线程的阻塞而极大影响性能。 可以考虑 aiohttp or httpx 都是比较热门的异步 io 库,httpx 同时支持同步和同步的client语法,而 aiohttp 仅支持异步。 另外如果项目已经使用了 request 需要注意以下几点:
直播与点播
直播与点播 直播(Live): 指客户端不能选择观看内容,只能观看服务端的提供内容 点播(VOD-VideoOnDemond): 客户端可以选择自己观看的内容 而这其中相关的两个主流协议:
RTMP(RealTime Message Protocol): 由 Adobe 创建的,最初的直播协议,支持也最受广泛,它基于 TCP 之上又封装了自己的协议,支持 推(Publish)/拉(Play) 模型,延迟低生态好,但是兼容性和网络穿透性差,只支持直播。可以参考一篇文章搞清楚直播协议RTMP HLS(HttpLiveSteam): 由 Apple 创建,兼容性和穿透性更好,它由索引文件 m3u8 + segment(ts)文件 组成,由于分片的组成,因此延迟也较大(等于分片大小),支持点播、直播,更多模式可以参考Example Playlist Files for use with HTTP Live Streaming 正常的mp4格式是不支持流式传输的,需要进行切片参考 How to output fragmented mp4 with ffmpeg?
ffmpeg基础用法 ffmpeg -i 要处理的文件 -o 输出文件名 -c:v 设置编码器,如libvpx-vp9, libxh264等 -c:a 设置音频编码 -crf h264/h265 的参数固定码率因子(CRF),取值 0~51 ,越低越好 -s 输出的分辨率如 1280x720 -r 输出帧率 -d 输出bit率 音视频基础知识 帧率:每秒视频输出的图片数量 码率:每秒输出的大小(如果帧率越高,输出的图片越多,在分辨率不变的情况,相应码率肯定会更高) 分辨率:视频内容的分辨率,表示由多少像素点组成 正常来说,帧率只要满足 24 fps ,人眼即感知不出来差异度,因此基本都是在控制分辨率和码率,在固定码率的情况下,如果还要加大分辨率,那么编码器只能对原内容进行一些阉割处理,比如色彩信息和打马赛克。
cgroup与容器化
cgroup与容器化 相信大家都知道 cgroup 是容器化的基础技术之一(这里我们指 runC 运行时,因为 kata 之类的容器技术使用的是虚拟化来进行隔离),而现今很多互联网应用都在朝云原生进行改造,因此无论是否从事容器相关的工作,我们都可以了解下cgroup的工作原理,便于我们更了解自己程序的允许环境以及容器化技术是怎么保障服务间的隔离性的。
cgroup是什么 cgroups 全称是 Linux Control Group,是内核的一个特性, 它运行研发以层次的结构来控制进程甚至是线程的资源用量(比如cpu、内存等),它的接口以一种伪文件系统(pseudo-filesystem)的形式暴露给用户使用(位于 /sys/fs/cgroup )。其中分组的层次关系由 内核代码 实现,而各个资源的限制与跟踪由其 子系统(subsystem) 来实现
详情可以参考 man7.cgroup
概念解释 开始前我们先介绍下一些cgroup相关的术语:
cgroup: 一个group,其中包含了受到相同资源限制的进程集合 subsystem: 子系统会对应内核的一个组件,它负责调整cgroup中的进程行为,以使它们满足预期。linux中已经实现了各种各样的子系统,它们使得我们可以控制进程的各个资源项。有时子系统也被称为 资源控制器(resources controller) or 控制器 hierarchy: 上文提到过,cgroup 会按照层次结构来组成,而这个组成拓扑被称为 hierarchy, 而每一个层级都可以定义自己的属性而限制在它之下的子层级,因此更高层的属性定义不能超过它的后代总和,比如你的父层级cpu限制为2c,你的后代层级之和为4c。 V1 与 V2版本 最初的cgroup 设计出现在 Linux2.6.24 中,随着时间发展,大量的控制器被增加,而逐渐导致控制器与hierarchy之间出现了巨大的不协调与不一致,详情可以参考内核文件:Documentation/admin-guide/cgroup-v2.rst (or Documentation/cgroup-v2.txt in Linux 4.17 and earlier)。 由于这些问题,linux从3.10开始实现一个正交的新实现(v2)来补救这些问题,直到 linux 4.5.x 才标记为稳定版本。文件 cgroup.sane_behavior,存在于v1 中,是这个过程的遗留物。该文件始终报告“0” 并且只是为了向后兼容而保留。 虽然 v2 版本是 v1 版本的一个替换,但是由于兼容性原因,它们都会持续存在于系统中,并且目前 v2 的控制器还只是 v1 版本的子集。当然,我们完全可以并行使用两者来完成我们的需求,唯一的限制是 v1 v2 的 hierarchy 不能存在相同的控制器(很容易理解,这是为了避免控制冲突。)
IAM系统调研
IAM系统调研 记录下最近对业界开源 IAM 系统的调研, IAM: IdentityAccessManagement,简单来说就是用户身份标识与访问管理,通常涉及两个部分:
认证(Authentication): 通常会涉及到用户登录等操作,用于识别 你是谁 授权(Authorization): 识别身份后就是授权的过程,用于管理 你能做什么 认证( Authentication )与授权( Authorization ) 正常的访问中,我们都会涉及到两个阶段:
认证:你是谁 授权:你能干什么 认证 上面已经提到,所谓认证简单来说就是“你是谁”,无论是颁发凭据的一方还是校验的凭据的一方,都是基于这个目的而行动。 常见的认证手段有:
密码 验证码 生物识别(指纹,人脸) 证书 第三方平台(OAuth2, OIDC) 而常见的认证协议:
Kerberos: 比较完全 LDAP: 轻量,还需要SASL去完善认证的流量 授权 授权是在认证之后识别出你能干什么,这里又分为两类知识:
授权模型:你怎么分配权限,通常用于系统资源的分配,比如某个用户是否具备某类资源的操作权限。 授权协议:你怎么授予他人使用自己的权限资源,通常用于 用户资源 在开放平台等场景使用, 常见的授权模型(复杂度逐步变高):
ACL(Access Control List): 将一组访问权限授予某个账号,类似文件系统的权限 RBAC(Role Base Access Control): 类似 ACL,但是不再授予给账号,而是角色,最后再将角色和账号绑定,更为灵活。但是缺点也很明显,多了一个角色管理对象。 Zanzibar: 谷歌云平台的权限控制模型,建立了一种他们自己的DSL(Object, Subject, Relation, SubjectSet 等),非常灵活,但是有一定的认知成本。 ABAC(Attribute Base Access Control): 基于属性的控制,相比 RBAC 的区别在于授予权限的对象变成一类满足特定要求的账号,比如 某个属性大于 X 的账号,给予 Y 权限。该模式最为灵活,但是配置太过复杂,因此业界几乎没用使用。 常见的授权协议有:
Droplet——一款轻量的Golang应用层框架
Droplet——一款轻量的Golang应用层框架 Github地址
如标题所描述的,Droplet 是一个 轻量 的 中间层框架,何为中间层呢? 通常来说,我们的程序(注意这里我们仅仅讨论程序的范围,而非作为一个系统,因此这里不设计如 LB、Gateway、Mesh等内容,因为它们都处于程序以外)按不同的职责可以分为不同的层次,而按照不同的设计风格,常见的如下:
三层架构:UIL(UserInterfaceLayer), BLL(BusinessLogicLayer), DAL(DataAccessLayer) DDD分层架构(参考ddd-oriented-microservice):ApplicationLayer,DomainLayer,InfrastructureLayer 洋葱架构(参考Onion Architecture ):Application, Infrastructure, ApplicationService, DomainService, DomainModel。 Tips
洋葱架构其实也是基于DDD的,它是DDD分层架构的升级版本。
但是今天我想用于解释中间层的架构并非以上的任何一种,它也源自于DDD的分层架构,不过我配合了六边形架构来说明它,分层图如下:
在六边形架构中有个规则:依赖只能是由外部指向内部。 因此从外层到最内层分别是:
分层 职责 Access 程序的接入层(在六边形架构中这被称为输入适配器),通常位于整个请求 or 任务的起点,它可能是某种Web框架,也可能是一些队列的消费框架等。 Application 程序应用层,包含了一些非业务的逻辑,如:业务逻辑的编排、参数绑定、校验、请求日志、链路上报、状态读取等等 Domain & Utils 在最中心的地方我放入了两个层次描述:Domain 与 Utils,这两个分层都应该是位于依赖的最底层,意味着他们不应该引用本项目的其他层次。Domain层主要包含核心的业务逻辑,而Utils则是一些程序任何地方都可能会引用的代码段,比如常量定义、数据结构和语法糖等等 Infrastructure 基础设施层(在六边形架构中这被称为输出适配器),程序所有需要对外进行信息交换 or 功能依赖时都会放置在这一层实现,通常来说这些功能都是被依赖的那部分,因此我们如果要满足依赖约束的话,这里必须要引入 DIP(Dependency inversion principle),即在Application、Domain中定义依赖,而 Infrastructure 来实现它们,这样保证了它们是可被替换的 六边形架构优点在于解耦程序中业务无关的部分,以保证它们都是可被替换与扩展的。 而 Droplet 就工作在 Application 层,它的核心能力只有一个:提供基于pipeline的请求/响应处理能力。 可能有人会疑问,几乎每个框架都会实现类似的能力,为什么我们需要 Droplet 呢? 别急,我们来看看这些框架自带的 pipeline/middleware 存在什么弊端。 根据上面的架构图我们可以知道诸如 gin、go-restful、fasthttp 之类的http框架都是工作在 Access 层,因此框架自带的 pipeline/middleware 存在以下两个弊端:
Nginx 配置最佳实践
Nginx 配置最佳实践 Buffer 注意Nginx作为代理服务器时的几个关键行为:
默认会缓存 request 的 body,直到接收完所有body后才会转发请求到后端 默认会缓存上游服务的 response,直到接收完所有 response 的body or body超过了设定值才会将请求转发给后端 这两个行为的目的都是为了克服慢client带来的影响,比如 client 在发起请求时很慢,大量的连接会贯穿到 upstream,而接收响应也是类似原理,nginx期望尽可能快地接收完 upstream 响应,以释放它的相关资源。 现在我们来详细看看过程中涉及哪些参数,以一个请求发起为例
client 发起请求,nginx 根据 proxy_request_buffering 判断是否需要缓存请求 body,默认为 on,即缓存请求 如果开启缓存,那么 nginx 会不断缓存 body 到内存中,大小为 client_body_buffer_size 所指定的大小,默认为两个内存页(曾经只有一个内存页大小)大小 8k(32bit os)/16k(64bit os),不过这个参数不一定适合现在的硬件了,参考 为什么 Linux 默认页大小是 4KB,如果超过这个大小,则会写入临时文件,路径为 client_body_temp_path 所配置。 1,2步骤完毕后,开始向后端转发,然后等待后端请求返回 开始接收 response ,根据 proxy_buffering 判断是否需要缓存响应 body,默认为 on,即缓存响应 如果开启缓存,和请求类似的是也有一个 proxy_buffers 来决定缓冲区大小,默认 8 4k(8 * 4k大小),当body大于这个buffer时会转储本地文件,文件大小为 proxy_max_temp_file_size 限制,每次写入大小为 proxy_temp_file_write_size,默认是 proxy_buffer_size 和 proxy_buffers 之和,当大于临时文件限制时将转为同步传输,同步传输将使用 proxy_buffers 定义的空间作为 buffer,同时还有一个 proxy_busy_buffers_size 用于控制 buffer 中的哪一部分用于回传给 client,因为传输的过程中,整个 buffer 都将被标记为 busy,不可用,因此为了实现边接边回传的均衡,建议 proxy_busy_buffers_size 不要大于 proxy_buffers 的一半。 如果关闭 nginx 的 buffering,那么nginx将使用 proxy_buffer_size 配置的 size 作为 buffer 来传输文件,可以通过提升这个值来加快传输速度
Fastflow——基于golang的轻量级工作流框架
Fastflow——基于golang的轻量级工作流框架 Fastflow 是什么?用一句话来定义它:一个 基于golang协程、支持水平扩容的分布式高性能工作流框架。 它具有以下特点:
易用性:工作流模型基于 DAG 来定义,同时还提供开箱即用的 API,你可以随时通过 API 创建、运行、暂停工作流等,在开发新的原子能力时还提供了开箱即用的分布式锁功能 高性能:得益于 golang 的协程 与 channel 技术,fastflow 可以在单实例上并行执行数百、数千乃至数万个任务 可观测性:fastflow 基于 Prometheus 的 metrics 暴露了当前实例上的任务执行信息,比如并发任务数、任务分发时间等。 可伸缩性:支持水平伸缩,以克服海量任务带来的单点瓶颈,同时通过选举 Leader 节点来保障各个节点的负载均衡 可扩展性:fastflow 准备了部分开箱即用的任务操作,比如 http请求、执行脚本等,同时你也可以自行定义新的节点动作,同时你可以根据上下文来决定是否跳过节点(skip) 轻量:它仅仅是一个基础框架,而不是一个完整的产品,这意味着你可以将其很低成本融入到遗留项目而无需部署、依赖另一个项目,这既是它的优点也是缺点——当你真的需要一个开箱即用的产品时(比如 airflow),你仍然需要少量的代码开发才能使用 为什么要开发 Fastflow 组内有很多项目都涉及复杂的任务流场景,比如离线任务,集群上下架,容器迁移等,这些场景都有几个共同的特点:
流程耗时且步骤复杂,比如创建一个 k8s 集群,需要几十步操作,其中包含脚本执行、接口调用等,且相互存在依赖关系。 任务量巨大,比如容器平台每天都会有几十万的离线任务需要调度执行、再比如我们管理数百个K8S集群,几乎每天会有集群需要上下节点、迁移容器等。 我们尝试过各种解法:
硬编码实现:虽然工作量较小,但是只能满足某个场景下的特定工作流,没有可复用性。 airflow:我们最开始的离线任务引擎就是基于这个来实现的,不得不承认它的功能很全,也很方便,但是存在几个问题 由 python 编写的,我们希望团队维护的项目能够统一语言,更有助于提升工作效率,虽然对一个有经验的程序员来说多语言并不是问题,但是频繁地在多个语言间来回切换其实是不利于高效工作的 airflow 的任务执行是以 进程 来运行的,虽然有更好的隔离性,但是显然因此而牺牲了性能和并发度。 公司内的工作流平台:你可能想象不到一个世界前十的互联网公司,他们内部一个经历了数年线上考证的运维用工作流平台,会脆弱到承受不了上百工作流的并发,第一次压测就直接让他们的服务瘫痪,进而影响到其他业务的运维任务。据团队反馈称是因为我们的工作流组成太复杂,一个流包含数十个任务节点才导致了这次意外的服务过载,随后半年这个团队重写了一个新的v2版本。 当然 Github 上也还有其他的任务流引擎,我们也都评估过,无法满足需求。比如 kubeflow 是基于 Pod 执行任务的,比起 进程 更为重量,还有一些项目,要么就是没有经过海量数据的考验,要么就是没有考虑可伸缩性,面对大量任务的执行无法水平扩容。