聊聊分布式事务

聊聊分布式事务 前言 ​ 随着业务的快速发展、业务复杂度越来越高,传统单体应用逐渐暴露出了一些问题,例如开发效率低、可维护性差、架构扩展性差、部署不灵活、健壮性差等等。 ​ 微服务架构是一个分布式的系统,按业务进行划分为独立的服务单元,解决单体系统的不足,同时也满足越来越复杂的业务需求。每个微服务仅关注于完成一件任务并很好地完成该任务。 微服务架构的特点 微服务架构的优势非常明显,在近些年迅猛发展。 将复杂的业务拆分成多个小的业务,能够达到更好的业务复用,有利于人员组织分工 服务独立部署,独立扩容,每个服务的修改和部署对其他服务没有影响 每个服务可以根据业务场景选取合适的编程语言和数据库 微服务有以上的优势,但是微服务也带来不少的新问题,例如: 服务数量众多,其测试、部署、监控等都变的更加困难。 单体应用拆分为分布式系统后,进程间的通讯机制和故障处理措施变的更加复杂 系统微服务化后,原先是一个服务内部的本地数据库事务,被拆到了多个服务,需要在分布式环境下保证事务的一致性 上述的各项问题中,1、2都可以通过近几年涌现的各项微服务技术解决,例如Kubernetes提供了服务发现、服务治理等。 因此分布式事务已经成为微服务落地最大的阻碍,也是最具挑战性的一个技术难题。 CAP 理论 一个分布式系统最多只能同时满足 一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。 分布式事务方案 分布式事务模式常见的有XA、TCC、SAGA、可靠消息。 ...

4 min · 1975 words · Luenci

Golang并发编程最佳实践

Golang 并发编程最佳实践 Goroutine 定义 ​ Goroutines 是与其他函数或方法同时运行的函数或方法。Goroutines 可以被认为是轻量级线程。与线程相比,创建 Goroutine 的成本很小。因此,Go 应用程序通常会同时运行数千个 Goroutine。 Goroutines 相对于线程的优势 与线程相比,Goroutines 非常便宜。它们的堆栈大小只有几 kb,堆栈可以根据应用程序的需要增长和缩小,而在线程的情况下,堆栈大小必须指定并固定。 Goroutine 被多路复用到更少数量的 OS 线程。一个包含数千个 Goroutine 的程序中可能只有一个线程。如果该线程中的任何 Goroutine 阻塞等待用户输入,则创建另一个 OS 线程并将剩余的 Goroutine 移动到新的 OS 线程。所有这些都由运行时处理,我们作为程序员从这些复杂的细节中抽象出来,并获得了一个干净的 API 来处理并发性。 Goroutines 使用通道进行通信。通道通过设计防止在使用 Goroutine 访问共享内存时发生竞争条件。通道可以被认为是 Goroutine 进行通信的管道。 ...

8 min · 3655 words · Luenci

Service Mesh 介绍

Service Mesh 介绍 Service Mesh 是微服务时代的 TCP/IP 协议 ​ 一种控制应用程序的不同部分如何相互共享数据的方法。与其他用于管理此通信的系统不同,服务网格是内置于应用中的专用基础结构层。这个可见的基础结构层可以记录应用的不同部分的交互程度(或交互程度),因此随着应用的增长,优化通信和避免停机变得更加容易。 – redhat ​ Buoyant的CEO William Morgan,也就是Service Mesh这个词的发明人,对Service Mesh的定义: ​ 服务网格是一个基础设施层,用于处理服务间通信。云原生应用有着复杂的服务拓扑,服务网格保证请求在这些拓扑中可靠地穿梭。在实际应用当中,服务网格通常是由一系列轻量级的网络代理组成的,它们与应用程序部署在一起,但对应用程序透明。 演化进程 时代0 开发人员想象中,不同服务间通信的方式,抽象表示如下: 时代1:原始通信时代 ​ 然而现实远比想象的复杂,在实际情况中,通信需要底层能够传输字节码和电子信号的物理层来完成,在TCP协议出现之前,服务需要自己处理网络通信所面临的丢包、乱序、重试等一系列流控问题,因此服务实现中,除了业务逻辑外,还夹杂着对网络传输问题的处理逻辑。 ...

5 min · 2198 words · Luenci

Golang error 处理最佳实践

golang error 处理最佳实践 错误类型定义 Go 中error 类型是一个接口类型 1 2 3 type error interface { Error() string } 基本上,error 是实现该接口任何内容,它将错误消息作为字符串返回。 构造错误 ​ 可以使用 Go 的内置或包动态构造错误。 ​ 例如,以下函数使用包返回带有静态错误消息的新错误:errors fmt errors 1 2 3 4 5 6 7 package main import "errors" func DoSomething() error { return errors.New("something didn't work") } ​ 同样,该包可用于向错误添加动态数据。 ​ 例如:fmt int string error 1 2 3 4 5 6 7 8 9 10 package main import "fmt" func Divide(a, b int) (int, error) { if b == 0 { return 0, fmt.Errorf("can't divide '%d' by zero", a) } return a / b, nil } 请注意,当用于用格式动词包装另一个错误时,这将非常有用 fmt.Errorf %w 在上面的示例中,还有其他一些重要事项需要注意。 错误可以返回为nil ,它是 Go 中 error 的默认值或零值。这很重要,因为检查是确定是否遇到错误的惯用方法(替换您可能在其他编程语言中熟悉的 / 语句)。if err != nil 错误通常作为函数中的最后一个参数返回。因此,在上面的示例中,我们按该顺序返回 int和 nil 。 当我们返回错误时,函数返回的其他参数通常作为其默认的零值返回。函数的用户可能期望,如果返回非nil 错误,则返回的其他参数不相关。 最后,错误消息通常以小写形式编写,不以标点符号结尾。但是可以例外,例如,当包含专有名词,以大写字母开头的函数名称等。 ...

9 min · 4119 words · Luenci

Go Project Layout 最佳实践

Golang Project Layout 最佳实践 鲍勃叔叔干净的架构(Uncle Bob) 依赖规则 同心圆代表软件的不同领域。 一般来说,你走得越远,软件的层次就越高。外圈是机制。内圈是政策。 ​ 使这个架构工作的最重要的规则是依赖规则。这条规则说源代码依赖只能指向内部。内圈中的任何人都无法对外圈中的事物一无所知。特别是,在外圈中声明的事物的名称不能被内圈中的代码提及。这包括函数、类。变量或任何其他命名的软件实体。 ...

7 min · 3201 words · Luenci

Go 变量声明指南

golang var、:=、new、make区别及使用 ​ go里面的几大变量“类型”(不严谨,只是个人在使用的时候常用到的结构的一个划分) (1) 值类型: int, string, struct 等 (2) 引用类型:主要是 map, slice,chan 这三个引用(make创建内存的) (3)指针类型:*int64, *struct等 var vs := ​ 对于值类型的变量,我们通过var 声明(包括结构体),系统会默认为他分配内存空间,并赋该类型的零值。 如下,我们声明一个int类型变量i,输出为0。 1 2 var i int fmt.Println(i) // i = 0 (类型零值) var和:=之间实际上存在差异,采用:=允许重新声明变量。 与常规变量声明不同,:=声明可以重新声明变量,前提是它们最初在同一块中以相同类型声明,并且至少有一个非空白变量是新的。因此,重新声明只能出现在多变量短声明中。 重新声明不引入新变量;它只是为原始值分配一个新值。 1 2 3 4 field1, offset := nextField(str, 0) field2, offset := nextField(str, offset) // 重新声明偏移量(可重入) a, a := 1, 2 // 非法:如果在别处声明了 a,则双重声明 a 或没有新变量 所以我会说:=运算符不是纯粹的声明,而是更像声明和分配。不允许在顶层重新声明,因此也不允许短声明。另一个原因可能是语法简单。type在 Go 中,所有顶级表单都以var 或 func 开头。 建议:重复声明的变量用 :=,比如 err 的声明,使用的值或全局变量(不推荐大量使用)用 var, 如 var fields []string ...

3 min · 1291 words · Luenci

服务配置和热更新

程序配置服务和热更新 前言 ​ 在开发过程中,因为不同环境中有不同的配置,所以往往一个项目要同时保存着不同环境的配置文件(dev,test,staging,prd)等。如果没有一个方便简洁的管理这些配置文件方式,排查问题也会变的麻烦。接下来介绍几种我所经历的几种配置文件管理方案 git 分支管理 ​ 顾名思义就是利用 git 的分支来管理不同环境的配置,比如dev分支就是对应存放这dev的配置文件。 优点 分支管理更符合开发的代码习惯,只关心本分支的代码和配置 缺点 不符合git-flow流程,如果test配置有改动,那么就要直接编辑test分支代码,而不是从dev分支合并过去。排查配置相关问题不友善 一份配置文件就一个分支,维护代价太大,有些舍本琢末了。 热更新方案无 所有配置文件都放在项目下 ​ 这种方式就是把所有的配置文件集中放在项目下的某个目录,用环境变量的方式去加载指定的配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package main func main(){ switch env{ case:"dev": load("dev.config") case:"test": load("test.config") ... default: load("local.config") } } 优点 配置统一集中管理,修改方便 缺点 配置文件过多容易使项目结构变的“难看”,判断依赖过多,不优雅。 无法做到热更新,配置更改需要重新发布代码 热更新方案无 配置中心 ​ 将配置文件都放到三方的服务中保管,比如nacos、Apollo等配置中心 nacos:https://nacos.io/zh-cn/docs/what-is-nacos.html Apollo:https://www.apolloconfig.com/#/zh/README 优点 集中化管理配置,配置文件“不落地” 有相关 sdk 调用,支持热更新等高级功能 缺点 要维护一个高可用的 三方服务 增加了维护成本 热更新方案 需要另外编码去开发 ...

3 min · 1342 words · Luenci

Golang Sync.Once 的探究

sync.Once 的用法 ​ 在多数情况下,sync.Once 被用于控制变量的初始化,这个变量的读写通常遵循单例模式,满足这三个条件: 当且仅当第一次读某个变量时,进行初始化(写操作) 变量被初始化过程中,所有读都被阻塞(读操作;当变量初始化完成后,读操作继续进行) 变量仅初始化一次,初始化完成后驻留在内存里 实例化一次客户端 ​ 在标准库中不乏有大量 sync.Once 的使用案例,在 strings 包中 replace.go 里实现字符串批量替换功能时,需要预编译生成替换规则,即采用不同的替换算法并创建相关算法实例,因 strings.Replacer 实现是线程安全且支持规则复用,在第一次解析替换规则并创建对应算法实例后,可以并发的进行字符串替换操作,避免多次解析替换规则浪费资源。 先看一下 strings.Replacer 的结构定义: 1 2 3 4 5 6 // source: strings/replace.go type Replacer struct { once sync.Once // guards buildOnce method r replacer oldnew []string } ​ 这里定义了 once sync.Once 用来控制 r replacer 替换算法初始化,当我们使用 strings.NewReplacer 创建 strings.Replacer 时,这里采用惰性算法,并没有在这时进行 build 解析替换规则并创建对应算法实例,而是在执行替换时( Replacer.Replace 和 Replacer.WriteString)进行的, r.once.Do(r.buildOnce) 使用 sync.Once 的 Do 方法保证只有在首次执行时才会执行 buildOnce 方法,而在 buildOnce 中调用 build 解析替换规则并创建对应算法实例,在 buildOnce 中进行赋值。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // source: strings/replace.go func NewReplacer(oldnew ...string) *Replacer { if len(oldnew)%2 == 1 { panic("strings.NewReplacer: odd argument count") } return &Replacer{oldnew: append([]string(nil), oldnew...)} } func (r *Replacer) buildOnce() { r.r = r.build() r.oldnew = nil } func (b *Replacer) build() replacer { .... } func (r *Replacer) Replace(s string) string { r.once.Do(r.buildOnce) return r.r.Replace(s) } func (r *Replacer) WriteString(w io.Writer, s string) (n int, err error) { r.once.Do(r.buildOnce) return r.r.WriteString(w, s) } ​ 简单来说,once.Do 中的函数只会执行一次,并保证 once.Do 返回时,传入 Do 的函数已经执行完成。多个 goroutine 同时执行 once.Do 的时候,可以保证抢占到 once.Do 执行权的 goroutine 执行完 once.Do 后,其他 goroutine 才能得到返回。 ​ once.Do 接收一个函数作为参数,该函数不接受任何参数,不返回任何参数。具体做什么由使用方决定,错误处理也由使用方控制,对函数初始化的结果也由使用方进行保存。 ...

2 min · 1001 words · Luenci

go get 私有库解决方案

golang go get 私有库解决方案 注意:go get 只支持 https协议的库路径 ‼️ 前言 ​ 在我们开发过程中会自己封装一些工具库,在某个项目中使用。但是如果有别的项目想使用你封装的库,那么此时你就需要将工具库封装为一个go module,给其它项目导入。一般公司内部的库是不对外开放的,这时候就需要搭建代理去拉取私有仓库 原理架构图 ...

2 min · 768 words · Luenci

聊聊链路追踪 OpenTracing

聊聊链路追踪 OpenTracing 什么是 Tracing 对 Tracing 的定义是,在软件工程中,Tracing 指使用特定的日志记录程序的执行信息,与之相近的还有两个概念,它们分别是 Logging 和 Metrics。 Logging:用于记录离散的事件,包含程序执行到某一点或某一阶段的详细信息。 Metrics:可聚合的数据,且通常是固定类型的时序数据,包括 Counter、Gauge、Histogram 等。 Tracing:记录单个请求的处理流程,其中包括服务调用和处理时长等信息。 同时这三种定义相交的情况也比较常见。 Logging & Metrics:可聚合的事件。例如分析某对象存储的 Nginx 日志,统计某段时间内 GET、PUT、DELETE、OPTIONS 操作的总数。 Metrics & Tracing:单个请求中的可计量数据。例如 SQL 执行总时长、gRPC 调用总次数。 Tracing & Logging:请求阶段的标签数据。例如在 Tracing 的信息中标记详细的错误原因。 针对每种分析需求,我们都有非常强大的集中式分析工具。 Logging:ELK,近几年势头最猛的日志分析服务,无须多言。 Metrics:Prometheus,第二个加入 CNCF 的开源项目,非常好用。 Tracing:OpenTracing 和 Jaeger,Jaeger 是 Uber 开源的一个兼容 OpenTracing 标准的分布式追踪服务。目前 Jaeger 也加入了 CNCF。 原理 ​ 分布式追踪系统大体分为三个部分,数据采集、数据持久化、数据展示。数据采集是指在代码中埋点,设置请求中要上报的阶段,以及设置当前记录的阶段隶属于哪个上级阶段。数据持久化则是指将上报的数据落盘存储,例如 Jaeger 就支持多种存储后端,可选用 Cassandra 或者 Elasticsearch。数据展示则是前端根据 Trace ID 查询与之关联的请求阶段,并在界面上呈现。 上图是一个请求的流程例子,请求从客户端发出,到达负载均衡,再依次进行认证、计费,最后取到目标资源。 请求过程被采集之后,会以上图的形式呈现,横坐标是时间,圆角矩形是请求的执行的各个阶段。 ...

11 min · 5147 words · Luenci

GO 代码风格指南

GO 代码风格指南 风格原则 ​ 有一些总体原则总结了如何考虑编写可读的 Go 代码。以下是可读代码的属性,按重要性排序: 清晰:代码的目的和基本原理对读者来说是清楚的。 简单性:代码以尽可能简单的方式实现其目标。 简洁:代码具有高信噪比。 可维护性:代码的编写使其易于维护。 一致性:代码与更广泛的 Google 代码库一致。 ...

23 min · 11159 words · Luenci

Giweights 介绍

Giweights 介绍 https://icloudnative.io/posts/what-is-giweights/ 基础设施即代码 在理解 Giweights 之前,我们需要先理解什么是基础设施即代码。 基础设施即代码(Infrastructure as Code, IaC),顾名思义,表示使用代码(而非手动流程)来定义基础设施,研发人员可以像对待应用软件一样对待基础设施,例如: 可以创建包含基础架构规范的声明式配置文件,从而便于编辑和分发配置。 可以确保每次配置的环境都完全相同。 可以进行版本控制,所有的变更都会被记录下来,方便溯源。 可以将基础设施划分为若干个模块化组件,并通过自动化以不同的方式进行组合。 当然,广义上的 IaC 不仅仅只关于基础设施,还包含了网络、安全、配置等等,所以广义上的 IaC 又叫 X as Code。 ​ 比如你想在 AWS 中创建服务器,配置网络,部署 Kubernetes 集群以及各种工作负载,你只需要定义好 Terraform 或 Ansible 的声明式配置,以及 Kubernetes 的配置清单即可,免去一切繁杂的手动操作。 Giweights 是什么 ​ Giweights = IaC + Git + CI/CD,即基于 IaC 的版本化 CI/CD。它的核心是使用 Git 仓库来管理基础设施和应用的配置,并且以 Git 仓库作为基础设施和应用的单一事实来源,你从其他地方修改配置(比如手动改线上配置)一概不予通过。 ​ Git 仓库中的声明式配置描述了目标环境当前所需基础设施的期望状态,借助于 Giweights,如果集群的实际状态与 Git 仓库中定义的期望状态不匹配,Kubernetes reconcilers 会根据期望状态来调整当前的状态,最终使实际状态符合期望状态。 ​ 另一方面,现代应用的开发更多关注的是迭代速度和规模,拥有成熟 DevOps 文化的组织每天可以将代码部署到生成环境中数百次,DevOps 团队可以通过版本控制、代码审查以及自动测试和部署的 CI/CD 流水线等最佳实践来实现这一目标,这就是 Giweights 干的事情。 Giweights vs DevOps ​ 从广义上来看,Giweights 与 DevOps 并不冲突,Giweights 是一种技术手段,而 DevOps 是一种文化。Giweights 是一种实现持续交付(Continuous Delivery)、持续部署(Continuous Deployment)和基础设施即代码(IaC)的工具和框架,它是支持 DevOps 文化的。 从狭义上来看,Giweights 与 DevOps 有以下几个区别: ​ 首先,Giweights 是以目标为导向的。它使用 Git 来维护期望状态,并不断调整实际状态,最终与期望状态相匹配。而 DevOps 更多关注的是最佳实践,这些实践可以普遍应用于企业的每一个流程。 ​ 其次,Giweights 采取声明式的操作方法,而 DevOps 同时接受声明式和命令式的方法,所以 DevOps 除了适用于容器环境之外,还适用于虚拟机和裸机环境。 ​ 最后,Giweights 重新定义了云原生场景下的 CI/CD,它以 Git 作为中心的不可变状态声明,以加快持续部署速度。 ...

6 min · 2986 words · Luenci

Django的logger配置

Django日志配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '%(levelname)s %(asctime)s %(pathname)s %(module)s %(lineno)s %(process)d %(thread)d %(message)s' } }, 'loggers': { 'django': { 'handlers': ['default'], 'propagate': True, 'level': 'DEBUG', 'filters': ['special'] } }, 'handlers': { 'default': { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'filename': '/var/logs/django/default.log', 'maxBytes': 1024 * 1024 * 5, 'backupCount': 5, 'formatter': 'verbose', 'filters': ['special'] } }, 'filters': { # 过滤器 'special': { # 使用自定义的web.my_logging.ContextFilter,别名special,可以接受其他的参数 '()': 'web.my_logging.ContextFilter' } }, } 配置分析说明 version 保留字。 disable_existing_loggers 是否禁用已经存在的logger实例。 如果LOGGING 中的disable_existing_loggers 键为True(默认值),那么默认配置中的所有logger 都将禁用。 Logger 的禁用与删除不同;logger 仍然存在,但是将默默丢弃任何传递给它的信息,也不会传播给上一级logger。所以,你应该非常小心使用'disable_existing_loggers': True;它可能不是你想要的。你可以设置disable_existing_loggers 为False,并重新定义部分或所有的默认loggers;或者你可以设置LOGGING_CONFIG 为 None,并 自己处理logging 配置。 Logging 的配置属于Django setup() 函数的一部分。所以,你可以肯定在你的项目代码中logger 是永远可用的。 formatters 定义输出的日志格式。 常用的格式化属性: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 %(name)s Logger的名字 %(levelname)s 文本形式的日志级别 %(message)s 用户输出的消息 %(asctime)s 字符串形式的当前时间。默认格式是 “2003-07-08 16:49:45,896”。逗号后面的是毫秒 %(levelno)s 数字形式的日志级别 %(pathname)s 调用日志输出函数的模块的完整路径名,可能没有 %(filename)s 调用日志输出函数的模块的文件名 %(module)s 调用日志输出函数的模块名 %(funcName)s 调用日志输出函数的函数名 %(lineno)d 调用日志输出函数的语句所在的代码行 %(created)f 当前时间,用UNIX标准的表示时间的浮 点数表示 %(relativeCreated)d 输出日志信息时的,自Logger创建以 来的毫秒数 %(thread)d 线程ID。可能没有 %(threadName)s 线程名。可能没有 %(process)d 进程ID。可能没有 其他格式化属性请参 LogRecord attributes ...

4 min · 1840 words · Luenci

从数据库中查询出来的结果一般是一个集合,这个集合叫做 QuerySet。 一、QuerySet何时被提交 在内部,创建、过滤、切片和传递一个QuerySet不会真实操作数据库,在你对查询集提交之前,不会发生任何实际的数据库操作。可以使用下列方法对QuerySet提交查询操作: 迭代 QuerySet是可迭代的,在首次迭代查询集时执行实际的数据库查询。 例如, 下面的语句会将数据库中所有Entry的headline打印出来: 1 2 for e in Entry.objects.all(): print(e.headline) 切片:如果使用切片的”step“参数,Django 将执行数据库查询并返回一个列表。 Pickling/缓存 repr() ...

26 min · 12928 words · Luenci

Golang 的值传递 先导文章:(正经版)面试官:切片作为函数参数是传值还是传引用? Go语言中的 new 和 make 主要区别如下: make 只能用来分配及初始化类型为 slice、map、chan 的数据。new 可以分配任意类型的数据; new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type; new 分配的空间被清零。make 分配空间后,会进行初始化; 传入参数和传出参数 传入参数为本身有值,传入函数让函数使用;传出参数本身没值,从函数中带出值(相当于函数的返回值)。 函数参数为指针 将指针作为参数传入某个函数时,函数内部会复制指针,也就是会同时出现两个指针指向原有的内存空间,所以 Go 语言中传指针也是传值。 传值 当我们验证了 Go 语言中大多数常见的数据结构之后,其实能够推测出 Go 语言在传递参数时使用了传值的方式,接收方收到参数时会对这些参数进行复制;了解到这一点之后,在传递数组或者内存占用非常大的结构体时,我们应该尽量使用指针作为参数类型来避免发生数据拷贝进而影响性能。 函数小结 通过堆栈传递参数,入栈的顺序是从右到左,而参数的计算是从左到右; 函数返回值通过堆栈传递并由调用者预先分配内存空间; 调用函数时都是传值,接收方会对入参进行复制再计算; ...

3 min · 1135 words · Luenci