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 执行任务的,比起 进程 更为重量,还有一些项目,要么就是没有经过海量数据的考验,要么就是没有考虑可伸缩性,面对大量任务的执行无法水平扩容。
分布式系统下的可用性与可伸缩性
分布式系统下的可用性与可伸缩性 背景 最近我们组开展了一个针对异地多活场景的通用解决方案——单元化,旨在解决分布式系统的灾备问题,在调查中了解到了很多业务有趣的解决方案,记录在本文中,方便回顾。
写下本文时,我正就职于字节跳动的中台架构组,负责为字节的各个业务中台的共性问题提供解决方案,比如 异地多活、多租户治理 等。
为什么要讨论这个话题 通过输出本文,让自己将杂乱的相关知识进行整理 随着互联网市场逐渐饱和,未来各个软件都将会更加注重自己的服务质量,比如可用性、性能、可拓展性等等,所以将会有越来越多的团队关注自己的服务质量,其中首当其冲的就是可用性,希望我的一些经验和见解能够帮助到后来的人 概念对齐 在开始正文前,我们有必要先对齐几个本文涉及到的概念:可用性与可伸缩性。
可用性 可用性是软件系统在一定时间内持续可用的时间,比如IT服务(像一些云平台or基础设施)会对他们的用户承诺 3个9, 4个9 之类,可用性相关的SLA,它们是什么含义? 这里的几个9指的是在一年时间内,可用时间所占比的百分率,比如 3个9 指, 在一年时间内,99.9% 时间都是可用的,那么对应的服务中断时间就是:
3个9:(1-99.9%)36524=8.76小时,表示该系统在连续运行1年时间里最多可能的业务中断时间是8.76小时。 4个9:(1-99.99%)36524=0.876小时=52.6分钟,表示该系统在连续运行1年时间里最多可能的业务中断时间是52.6分钟。 5个9:(1-99.999%)36524*60=5.26分钟,表示该系统在连续运行1年时间里最多可能的业务中断时间是5.26分钟。
有人可能会有疑问,软件跑的好好的为啥会中断?中断的原因可能会有很多种:
服务更新:如果版本升级时没有实现平滑升级,那么会造成短暂的中断 机器故障:比如机器太老,需要裁撤,突然网卡坏了,机房散热器故障等等,如果你的服务没有健全的健康检查以及切流机制,往往就会带来灾难性的影响 通常来说,软件设计层面上的缺陷都是可以避免的,比如服务更新造成的中断。所以一般评估可用性都是在物理层面的影响范围上来看待,因为物理层面上的故障原因是不可控的,比如机器老化、自然灾害等。 按照范围来说,我们可以简单对软件的可用性进行一个分级
单实例:顾名思义,单个实例运行,最低的可用性级别,没法防范任何范围的故障 机房级别容灾:可以抵御机房级别的故障,通常来说都是多个实例分散在不同机房来实现 数据中心级别容灾:一般一个数据中心由多个机房组成,这个级别的容灾可以容忍一个数据中心故障 城市级别容灾:跟名字一样,能够容忍一个城市的所有数据中心出现灾害 国家级别容灾:不解释了。 这几个级别容灾能力从上往下依次递增,所需要的解决的问题和付出的代价也有很大不同,这里多说几句:
机房级别容灾 一般很少见,因为多数基础设施的提供商都不会暴露机房信息给你,而且这个粒度也偏细了 数据中心级别容灾 在各个云上可以类比成可用区(AZ:AvailiableZone),每个可用区都是独立的物理资源,以尽量做到故障隔离,但实际以我在腾讯游戏维护容器平台时的经验来看,其实很难做到 可伸缩性 可伸缩性其实比较容易理解,就是指一个服务能否扩缩容,其中又包含了两个方向:
垂直伸缩: 简单来说就是加资源,4CPU换8CPU,8G换16G,这在虚拟机时代是个比较重的操作,但是在容器环境下已经非常轻量了,但是受限于单台服务器的配置,你不可能无限大地垂直扩容 水平伸缩:需要架构支持,能够通过添加实例来分担负载,这里架构上要克服的问题就是当多个实例同时运行时,并发所带来的各种bad case。 这两种伸缩方式,前者是不需要软件进行任何修改的,天然支持;而后者是处理海量数据、请求时所必须实现的。 可伸缩性的评估可以按耗时:
不可伸缩:顾名思义,只能单实例运行,不具备可伸缩性 秒级:扩缩容能够在秒级别完成 分钟级:扩缩容能够在分钟级别完成 小时级:扩缩容能够在小时级别完成 天级:扩缩容能够在天级别完成 可能有同学疑问,为啥伸缩性只看时间,不看成本,比如是全自动,还是半自动或者纯人肉扩容,确实对于可伸缩性来说,自动化也很重要,但自动化是比较难量化的工作,时间是可以量化的。 另外换个角度看待这个问题,能在分钟级别甚至秒级别完成的扩缩容机制,它能靠人肉实现吗?
golang如何更优雅地处理Error
golang如何更优雅地处理Error 今天想要分享的是,golang中如何更优雅地处理错误。
怎么处理Error是golang中一个非常关键的事情,因为golang的设计导致代码中到处都是类似以下的代码
if err != nil { ... } 如果处理不得当,会导致代码膨胀得非常快且难以维护,比如:
if err != nil { metrics.Emit(...) log.Print(...) event.Emit(...) ... ... return err } 面对这样的代码,可能错误处理所占的代码行数都会多于逻辑代码,显然不是我们愿意看到的。 上面描述的代码膨胀现象只是错误处理中常出现的问题之一,接下来我们聊聊日常开发中该如何优雅地处理错误。
错误的处理方式 错误的传递方式无非两种:
返回 不返回 选择返回or不返回的场景无非几种:
当函数位于顶层,比如 API的接入层、conumer的handle 等,此时无法返回 发生的错误是 致命的,会影响到整个程序的运行,此时应该抛出panic,阻止程序发生更加不可控的事情 发生的错误是 预期的,比如查重动作中查询数据库的数据不存在时,得到了一个 NotFound 的错误 其他情况均应该返回错误 而面对错误发生时,常见有以下的处理方式:
记录(日志、metrics、事件) 做一些业务逻辑 直接返回 这里只对 记录 单独展开说下,其他两项暂时没什么需要注意的。
记录 记录的常见手段有三种:log, metrics, event。 通常来说我们只需要记录其中一种即可,有的同学可能会有疑问,明明这三种不同的技术都有不同的适用场景,为什么说通常了一种就够了。 首先我们要聊聊这三个记录的核心目标是什么:
log: 最传统的形式,是为了在程序运行时留下可追溯的信息,来辅助人类了解程序发生了什么,一般都会进行分级处理。 metrics: 由于不同的 metrics 体系,实现不同,这里只概述下,metrics是用于观测某个属性的趋势 event: 用于敏感操作的审计 or 广播变更 我在过往的工作中,见过某些同学,在某个很关键的场景出错后采用了以下的做法: