Openresty 笔记

Openresty 笔记 简介 Openresty 是一个国人将LuaJIT嵌入Nginx进程进而可以使用Nginx来进行开发高性能的Web框架。 入门的简介可以参考这个文档,OpenResty 不完全指南 注意点 Openresty 由于是在每个 nginx worker 都运行了一个 luajit 所以它仍然是一个单线程模型所以不存在 并行(parall) 问题,但是用于处理请求的是 lua 的 协程coroutine,那么这里可能存在 并发(parallelism) 问题,即如果 协程A 因为某种原因挂起后yield(IO操作/sleep),此时 协程B 将会被运行,同时共享 模块变量 以及 全局变量。openresty 这种非抢占式的协程调度带来了优点,也有缺点,优点就是不存在多个协程同时进行导致的并发读写问题,缺点在于如果某个协程进行一些长时间的调用,比如计算或者系统调用等,那么整个进程都会被卡主,导致吞吐量大大下降。(go 在 1.14 之后实现了基于信号的抢占式协程调度,由 sysmon 线程来执行检测,每 20us 检测周期,如果发现P的running状态超过 10ms or syscall 超过 20us,那么直接发送信号给 M,让其放弃正在执行的 G,开始执行其他的 G) 使用 Openresty 可以调用 c 的代码,底层是利用了 linux 的 dlsym 和 dlopen 利用后者加载链接库,前者加载函数符号。由于 LuaJIT 与 C 进行交互,所以不会识别 c++ 的机制,比如析构函数,这样一来你就没法使用 c++ 推荐的 RAII 机制。你有两个方式来管理内存:1、c way,在使用完后释放他,这是你的责任 2、lua way 可以在 lua侧包装一下返回的类,再使用 ffi.
查看更多 →

如何在Go中优雅地进行测试

如何在Go中优雅地进行测试 本文是 快速迭代中保证项目的高质量——DevOps实践漫谈,一文中关于测试的延伸阅读,主要展示在 go 项目中该如何去做测试,以及测试是如何驱动设计的演化。 虽然是以 go 为示例,但里面的多数测试方法和思想都与语言无关,不会 go 的同学也可以阅读。 简述 测试的重要性这里不多复述,可以参考我关于 单元测试的必要性?由谁来写 的 回答。 测试分为很多种,本文这次要讨论的是关乎后台服务最重要的两种测试:单元测试 与 集成测试。它们两属于测试金字塔图的最下两层,如下图: 图中位于第二层的测试名为服务测试(Service Tests) ,其实它职责包含了狭义的集成测试,我将其理解为广义的集成测试。 开始进入主题之前,我觉得应该和阅读的同学对齐一下一些概念,因为关于测试的术语网上各种概念都各不相同,我这里只列举本文会使用到的一些概念( 这些是我的理解,并不一定完全恰当与正确 ): 模块: 指逻辑上紧密耦合的一组函数,通常它们会以对象形式实现,以接口的形式提供给其他模块使用。 外部环境:数据库,文件系统,其他服务等 单元测试:对于单个模块的测试,此时应该通过桩模块隔离被测模块以外的模块 桩(stub):用于在单元测试中替换被测模块的依赖模块,使其总是返回我们期望的值 集成测试:狭义的集成测试只测试那些访问外部环境的代码,比如: 读写数据库 调用外部服务的 API 读写队列 读写文件 广义的集成测试指对整个服务进行测试,包括外部环境与所有模块。 从概念上我们可以看出集成测试是最能保证业务价值的,但是由于需要集成的东西过多,所以它的执行很缓慢且用例容易失效(任何一个子模块的变动都可能让相关用例的数据不再有效),维护成本很高。所以测试金字塔为我们指出了测试中的一个最佳实践:使用单元测试保障更多的边界境况,集成测试来确保模块与模块,模块与外部环境的正常集成 接下来,让我们一起来看看在一个 go 项目中该如何落地这个最佳实践。 从一个简单的业务开始 让我们从一个简单的银行来开始我们的实践,假设有以下需求: 用户需要用一个手机号码来创建一个账户,支持更换手机号 支持付款、转账、销户 需要记录账单流水 我们的需求非常简单,但已经足够用于展示了。 基础环境准备 由于需求的简单性,这里我们直接跳过建模分析的过程,把DDL放在下面: SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for accounts -- ---------------------------- DROP TABLE IF EXISTS `accounts`; CREATE TABLE `accounts` ( `id` int(11) NOT NULL AUTO_INCREMENT, `phone` varchar(16) NOT NULL, `balance` double NOT NULL, `status` varchar(16) NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for bills -- ---------------------------- DROP TABLE IF EXISTS `bills`; CREATE TABLE `bills` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `desc` varchar(128) NOT NULL, `account_id` int(11) NOT NULL, `amount` double NOT NULL, `created_at` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 开始开发 本次的业务场景采用 DDD(领域驱动设计) 的设计风格开发,但不会展开说明,因为这是一个很大的话题,有兴趣了解的同学可以找我私聊,或者评论留言,我可以把自己过往实践 DDD 的经验再单独写一篇文章。
查看更多 →

网络问题杂谈

本文记录了一些工作中常见的网络问题以及网络工具使用方法,包括:NAT是什么、tcpdump用法、connect超时等。 在 linux 上我们可以使用 tcpdump 来分析流量包,wireshark 分析包,strace 查看进程调用。 fiddler 用于分析 http 协议,wireshark 用于分析 tcp/udp 的网络封包 https 包抓取 网络流量的这些工具为了能够解析 https 的包,通常都是自己签发证书,然后让系统信任自己证书,以作为中间人去转发、解析 https 流量。 如果仅仅只是为了代理 https 流量而不解析,可以使用 http 的隧道协议,使用 CONNECT method 去连接代理服务器,然后代理服务自动转发握手请求,相当于客户端直接与目标服务连接。 NAT(Net Address Translation) NAT 常用于虚拟化技术中,又分为三类: 静态NAT:此类NAT在本地和全局地址之间做一到一的永久映射。须注意静态NAT要求用户对每一台主机都有一个真实的Internet IP地址。 动态NAT:允许用户将一个未登记的IP地址映射到一个登记的IP地址池中的一个。采用动态分配的方法将外部合法地址映射到内部网络,无需像静态NAT那样,通过对路由器进行静态配置来将内部地址映射到外部地址,但是必须有足够的真正的IP地址来进行收发包。 地址端口NAT(NAPT):最为流行的NAT配置类型。通过多个源端口,将多个未登记的IP地址映射到一个合法IP地址(多到一)。使用PAT能够使上千个用户仅使用一个全局IP地址连接到Internet。NAPT 又根据转换源头或者目标不同又分为 SNAT 和 DNAT。实现 NAPT 的常见手段是设备维护一张转换表,里面存储了由 源地址:端口 到 转换后地址:端口 的映射关系 Tips SNAT, DNAT, MASQUERADE都是NAT, MASQUERADE是SNAT的一个特例, SNAT是指在数据包从网卡发送出去的时候,把数据包中的源地址部分替换为指定的IP,而 MASQUERADE 则是用指定用网卡 IP 来替换。最开始了解 SNAT 和 DNAT 时觉得两者就是相互的,一旦产生了一个 SNAT,回包时就必须有 DNAT 的操作,但是事实上区分两者时是以发送数据包的动作来区分,而不是接收。因为路由是按照目的地址来选择的,因此DNAT是在PREROUTING链 上来进行,而SNAT是在数据包发送出去的时候才进行,因此是在POSTROUTING链上进行的
查看更多 →

互斥性与幂等性

互斥性与幂等性 我觉得这两个特性是在系统分析和设计时必须考虑的问题。开始之前,先简单解释下两者的概念。 什么是互斥性 要解释互斥性就不得不先提一下 临界资源,引用百科的介绍。 一次仅允许一个进程使用的资源称为临界资源。 很显然,当多方共享临界资源时,那么这些使用方是互斥的。 举一些场景: 多个人同时修改同一个文档 多个线程一起修改一个并发不安全的Map 假设程序不保证互斥性: 让大家一起修改,那么: 场景一:最后只会保留最后一个处理的修改,其他的修改都会丢失 场景二:在多线程同时写入内存时会因为竞争关系出现数据错乱,有兴趣的同学可以看下这篇文章 为什么你的并发程序不安全。 这些显然是我们不愿看见的情况,所以保证互斥性对于临界资源的使用是非常重要的,保证互斥性的过程其实也是保证并发安全性的过程。 这里再多说下一些进程内的情况,比如多个线程对一个变量的读写,一写多读,需不要保证互斥性? 答案是要。 这里涉及到两个原因: 哪怕你的语句是一条汇编的原子操作(比如MOV xxxx, xxxx) 都不一定是原子操作。 多线程操作时,可能位于不同CPU上执行,修改对其他在其他CPU上执行的线程来说并不是马上可见的 基于以上原因,那怕是对一个bit的操作,也需要考虑互斥性。 什么是幂等性 幂等原本是数学的概念,表示某个运算执行任意多次与一次的结果相同。 后来引用到系统设计中,指具备该性质的接口在相同的参数下,调用一次与多次结果相同。 显然,有一些类别的接口天生就是幂等的,比如 查询 和 删除。 幂等性的重要性在于消除重复提交来带的副作用。 考虑下面的场景 某个人在同一页面多次提交对文档的修改(假设页面没有二次防押) 某个秒杀环节中,通过消息中间件来分摊处理的压力,但是消息中间件由于各种不可控因素投递了两次相同的消息 假设接口不保证幂等性,那么: 场景一:加大了存储系统的负担,如果同一时间很多这样操作的用户,可能会引发服务雪崩。 场景二: 如果消费的逻辑没有保证幂等,那么就会生成两条相同的订单 互斥性的解决方案 引用下在美团技术沙龙的 分布式系统互斥性与幂等性问题的分析与解决 出现的一句话 基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。 我觉得这句话很棒,在 «七周七并发模式» 书中出现的并发模式比如 CSP, Actor其实本质上也都是把对共享资源的请求串行化了,比如 CSP 是将请求都放入 Channel 中,而 Actor 则放入了 MailBox。
查看更多 →

分布式事务笔记

分布式事务笔记 分布式事务的常见解决方案: 2PC: perpare + commit 3PC: can_commit + pre_commit + do_commit TCC: try + confirm + cancel Saga: Events/Choreography or Command/Orchestration 本地消息表: local + event 2PC 2PC 由协调器通知各个事务参与者,由 perpare 与 commit 两个阶段组成,前者通知所有参与者预备事务,后者通知参与者真实提交事务。如果有参与者在 prepare 阶段失败,那么会通知所有参与者回滚。 优点: 步骤少,简单 缺点: 协调器单点故障 会阻塞其他参与者的事务 commit 阶段可能出现消息丢失 3PC 3PC 在 2PC 的基础上增加了各个参与者的超时时间,如果参与者在超时是时间内没有相应,那么视为失败。3PC 依然没有解决 commit 消息丢失的问题。 TCC TCC 相当于 3PC 的完善,除了超时机制外,还存在 cancel 步骤,意味着可以在 commit 失败后进行补偿。 Saga Saga 是一种长活事务的设计模式( Long-live transaction ),它基于 1987 年的一篇论文,主要有两种实现:Events/Choreography or Command/Orchestration 可以参考:saga-pattern-implement-business-transactions-using-microservices-part、 saga。
查看更多 →

分布式一致性笔记

查看更多 →

微服务最佳实践

微服务最佳实践 本文记录了工作当中积累的一些关于微服务最佳实践的想法 一定要做到的点 每个请求一定要有 request-id,一般由调用方生成,如果你是在推动一个遗留项目,那么可以在网关或者AOP层去生成它,并且在日志会和调用链路中携带上这个ID,这样才能够在繁杂的日志中找到特定的请求信息。建议由 uuid 或者 雪花算法 生成。 请求日志和错误日志分离,这么是因为这两者具有完全不同的关注点,且前者格式相对固定。分离他们可以在清洗日志时使用不同的格式。 当请求发生错误时(无论是预期内的还是预期外)都应该体现在日志当中 如果你们的系统引入了调用链追踪,那么可以使用 request-id 作为 trace-id。 对于每一个资源的操作都应有反馈,比如删除不存在的资源或者创建同名的资源,我们都应该返回特定的错误码,通常可以使用三类: NotFoundError, ConflictError, InternalError,ValidationError,同时错误码应该有所区分——预期和非预期的(比如使用code的范围去区分是一个选择),以便于落地告警 网关和微服务打印日志建议分开侧重点,网关应该打印协议层的细节,比如 http header, http code, cookie等,而微服务应该打印 应用Code, 请求参数,响应参数。这样可以在网关统计到后端微服务有多少是因为转发时的网络问题而导致访问失败,多少又是因为应用服务内的问题而失败。 系统和服务都应该有明确的边界,服务边界很容易理解,就是一个独立的部署单元,系统的边界是指对所有系统外的服务的交互都应该收口,这样才能更好的进行流量治理。理想情况下我们可以通过网络策略来形成物理边界,比如k8s的namespace。但是在真实的情况中可能你要面对很多遗留服务,它们已经通过IP地址或者服务发现互相访问了,直接去改造它们成本非常大,那么你可以在逻辑上形成边界,比如提炼一个新的概念对象,将现存的服务都分门别类,然后再请求上就可以区分出流量到底来自外部还是内部。不过值得注意的是,物理边界虽然理想,但是在做异地多活时可能会面临一些挑战,比如你使用namespace来作为网络边界,那么你需要把不同地区和机房的机器都包含到一个集群中才能满足不同程度的容灾需求。 谨慎对待 Error 和 Warning 级别的日志,它们可以用于反馈当前服务的健康度,可以对其进行监控和告警,同时为了控制好整个程序中的错误级别,建议不要在代码中hardcode打印日志,统一返回到上层去区分处理。 乐观锁可以使用最后更新时间或者http标准下的ETag 请求时一定要注意超时时间,尤其是调用链路比较长时,要保证 client 和 server 的 context 声明周期一致,通常来说,client 不应该设置自己的超时时间,统一由服务端控制。但是如果基础设施不稳定的,话,这种方式可能会导致较长的无效等待,为了达到更高的可用性,全链路超时是必须的,超时不由服务端决定,而是客户端决定。 在没有使用 ThreadLocal 技术的项目中。无论在开发or基础库都应该注意日志打印时预留context参数,便于日志染色 枚举的转义关系应尽量交给客户端来渲染,客户端可以从枚举的维护方自行下载缓存到本地做映射,这里有几个考虑 暴露枚举值的转义表之后,各个接入方都可以自行转义,包括前端。如果你期望将转义关系隐藏在后端,只有当你的枚举值不被其他服务所关心时才能这样做,且没有带来太多的价值。 每个消费房对枚举转义形式可能期望不同,比如一个数组的表现形式,树当中一个节点的表现形式等,都可能不同 如果枚举的定义并不是你的服务,且业务逻辑并没有依赖这部分数据,这时仅为了满足某个消费方(通常来说是前端orApp)去依赖其他业务接口来完成转义,其实是引入了更多风险,并且没有必要,因为需要这个转义的消费方多数时候其实已经获得过转义表 对于预期外的行为,服务必须要有响应,比如: 获取、更新、删除一个不存在的记录 写入冲突 项目结构(project layout) 这真是一个非常常见的问题了,我觉得这个问题需要结合场景来看待: 编程语言:每个语言都有自己比较常见的项目结构,比如 golang,在各自的标准下的结构才是我们需要考量如何区分的。 架构风格:由于解决的问题不同,你的软件可能会采用不同的架构风格,比如 数据流式(批处理,管道),请求/响应式(N层,面向对象),独立组件式(交互进程,事件驱动),数据中心式(数据库,黑板系统)可以参考 Architectural-Styles-Patterns。使用不同架构风格时,你的项目结构也会调整。 总的来看项目结构很难以一个万金油的模版去一统天下,都需要结合各自语言的特性以及不同架构风格来解决,这里提供一些常见选择:
查看更多 →

api压力测试工具简介

api压力测试工具简介 这里介绍两个我常用的测试工具:vegeta 和 wrk,现在我一般都用前者 Vegeta( 贝吉塔 ) 这是一个开源软件,类似 wrk ,但是是用 go 写的,功能更为强大,也更易用一些。 Github 地址 下面是命令列表: Usage: vegeta [global flags] <command> [command flags] global flags: -cpus int Number of CPUs to use (defaults to the number of CPUs you have) -profile string Enable profiling of [cpu, heap] -version Print version and exit attack command: -body string Requests body file -cert string TLS client PEM encoded certificate file -chunked Send body with chunked transfer encoding -connections int Max open idle connections per target host (default 10000) -duration duration Duration of the test [0 = forever] -format string Targets format [http, json] (default "http") -h2c Send HTTP/2 requests without TLS encryption -header value Request header -http2 Send HTTP/2 requests when supported by the server (default true) -insecure Ignore invalid server TLS certificates -keepalive Use persistent connections (default true) -key string TLS client PEM encoded private key file -laddr value Local IP address (default 0.
查看更多 →

Linux系统小记

Linux系统小记 修改环境变量 可以通过 export var=value 在当前控制台修改,不会永久生效 可以通过 以下文件 修改 ,添加 export var=value ============ /etc/profile ============ 此文件为系统的每个用户设置环境信息,当用户第一次登录时,该文件被执行. 并从/etc/profile.d目录的配置文件中搜集shell的设置. =========== /etc/bashrc =========== 为每一个运行bash shell的用户执行此文件.当bash shell被打开时,该文件被读取. =============== ~/.bash_profile =============== 每个用户都可使用该文件输入专用于自己使用的shell信息,当用户登录时,该 文件仅仅执行一次!默认情况下,他设置一些环境变量,执行用户的.bashrc文件. ========= ~/.bashrc ========= 该文件包含专用于你的bash shell的bash信息,当登录时以及每次打开新的shell时,该文件被读取. ========== ~/.profile ========== 在Debian中使用.profile文件代 替.bash_profile文件 .profile(由Bourne Shell和Korn Shell使用)和.login(由C Shell使用)两个文件是.bash_profile的同义词,目的是为了兼容其它Shell。在Debian中使用.profile文件代 替.bash_profile文件。 ============== ~/.bash_logout ==============当每次退出系统(退出bash shell)时,执行该文件 常用命令 查找文件是否包含字符串 grep -rn "string" /path/to/search 查找文件名 find /path/to/search -name "string" 查看磁盘信息 df -h 查看文件夹大小(前者仅仅查看目录,后者查看目录以及所有子项) du -sh /path/to/look du -h /patt/to/look 查看 tcp 网络 netstat -atlnp 查看相关信息 -查看CPU型号
查看更多 →

Leader选举机制

查看更多 →