0%

Go Clean Architecture 实现

之前阅读了《Clean Architecture》,结合一些个人理解,用Golang实现一个实现整洁架构的业务开发框架,我们回顾下整洁架构的主要要素:

  • 框架独立性,不依赖于特定的软件库框架,这样你可以像工具一样使用这种框架,而不需要让你的系统受到它的约束条件;
  • 可测试性,可以在没有 UI、数据库、Web 服务器或任何其他外部依赖的情况下可以测试业务规则;
  • UI独立性,UI 可以轻松更改,而无需更改系统的其余部分;
  • 数据库独立性,可以将 Oracle 或 SQL Server 换成 Mongo、BigTable、CouchDB 或其他东西;
  • 外部依赖独立性,事实上业务逻辑代码不应该和外部具体实现打交道;

Bob大叔的整洁架构主要包含四层:

  • Entity
  • Usecase
  • Controller
  • Framework & Driver

golang clean architecture

我们结合golang项目推荐做法和Bob大叔的整洁架构思想来做项目划分

Read more »

Clean Architecture

本文翻译自Robert C. Martin的博文The Clean Architecture

在过去的几年里,我们已经看到了一系列关于系统架构的想法,这些包括:

尽管这些架构在细节上都有所不同,但它们非常相似。它们都有相同的目标,即关注点分离。他们都通过将软件分层来实现这种分离,这些架构里面至少有一个业务层,其他都是接口层。

这些架构都有以下特性:

  • 框架独立性,不依赖于特定的软件库框架,这样你可以像工具一样使用这种框架,而不需要让你的系统受到它的约束条件;
  • 可测试性,可以在没有 UI、数据库、Web 服务器或任何其他外部依赖的情况下可以测试业务规则;
  • UI独立性,UI 可以轻松更改,而无需更改系统的其余部分。例如,可以用控制台 UI 替换 Web UI,而无需更改业务规则;
  • 数据库独立性,可以将 Oracle 或 SQL Server 换成 Mongo、BigTable、CouchDB 或其他东西,业务规则未绑定到数据库;
  • 外部依赖独立性,事实上业务逻辑代码不应该和外部具体实现打交道;

img

The Dependency Rule

同心圆代表软件的不同领域,一般来说,越往外圈软件的层次就越高,外圈是机制,内圈是政策。

使这个整洁架构工作的最重要的规则是依赖规则。这个规则说明了源代码依赖只能向内。内圈中结构不需要知道外圈具体实现,特别是,在外圈中声明的事物的名称不能被内圈中的代码提及。这包括函数、类、变量或任何任何实体信息。

Read more »

Go 依赖注入

软件工程中,依赖注入(dependency injection,缩写为 DI)是一种软件设计模式,也是实现控制反转的其中一种技术。这种模式能让一个物件接收它所依赖的其他物件。“依赖”是指接收方所需的对象。“注入”是指将“依赖”传递给接收方的过程。在“注入”之后,接收方才会调用该“依赖”[1]。此模式确保了任何想要使用给定服务的物件不需要知道如何建立这些服务。取而代之的是,连接收方物件(像是 client)也不知道它存在的外部代码(注入器)提供接收方所需的服务。

该设计的目的是为了分离关注点,分离接收方和依赖,从而提供松耦合以及代码重用性

from wiki

以上wiki是从概念上来解释Dependency Injection,对于初学者一看有点晕,其实道理很简单,一个函数需要传入各种参数来实例化一个对象,现在使用Dependency Injection方式的话,直接传入该对象作为参数。这就是所谓的『依赖是注入进来的』,而和它的构造方式解耦了。构造和销毁这些『控制』操作也交给了第三方,也就是『控制反转』。

在 Go 中,这通常采用将依赖项传递给构造函数的形式:

1
2
// NewUserStore returns a UserStore that uses cfg and db as dependencies.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

使用go进行业务开发,刚开始的时候项目通常依赖不多,随着需求以及开发人员越来越多之后,项目依赖也随着增加,而且依赖之间还有一定的先后依赖关系,甚至还有一些隐式的依赖关系。如果有趁手的依赖管理框架的话,可以大大减少开发人员的心智负担。

golang依赖注入框架有两个派别,一种是通过反射在运行时进行依赖注入,代表是Ubser的dig开源项目,另一种是进行代码生成在编译的时候完成注入,代表是google的wire项目。使用 dig 功能会强大一些,但是缺点就是错误只能在运行时才能发现,使用 wire 的缺点就是功能限制多一些,但是好处就是编译的时候就可以发现问题,wire使用者相对会多点,本文主要阐述的也是基于wire的依赖管理。

What is Wire

正式开始前需要先了解一下 wire 当中的两个概念:provider 和 injector

Provider 是一个普通的函数,这个函数会返回构建依赖关系所需的组件。如下所示,就是一个 provider 函数,在实际使用的时候,往往是一些简单的工厂函数,这个函数不会太复杂。

1
func NewCartUseCase(repo CartRepo, logger log.Logger) *CartUseCase

injector 也是一个普通函数,我们常常在 wire.go 文件中定义 injector 函数签名,然后通过 wire 命令自动生成一个完整的函数

1
2
3
4
5
// +build wireinject

func GetCartService() *Blog {
panic(wire.Build(NewCartService, NewCartUsecase, NewCartRepo))
}

第一行的 //+build wireinject 注释确保了这个文件在我们正常编译的时候不会被引用,而 wire . 生成的文件 wire_gen.go 会包含 //+build !wireinject 注释,正常编译的时候,不指定 tag 的情况下会引用这个文件。

wire.Buildinjector 函数中使用,用于表名这个 injector 由哪些 provider 提供依赖, injector 函数本身只是一个函数签名,所以我们直接在函数中 panic 实际生成代码的时候并不会直接调用 panic。

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
34
35
36
37
package example

// simple wire example, not used in production

// ICartRepo ICartRepo
type ICartRepo interface{}

// NewCartRepo NewCartRepo
func NewCartRepo() ICartRepo {
return new(ICartRepo)
}

// usecase

// ICartUsecase ICartUsecase
type ICartUsecase interface{}

type cartUsecase struct {
repo ICartRepo
}

// NewCartUsecase NewCartUsecase
func NewCartUsecase(repo ICartRepo) ICartUsecase {
return cartUsecase{repo: repo}
}

// service service

// CartService CartService
type CartService struct {
usecase ICartUsecase
}

// NewCartService NewCartService
func NewCartService(u ICartUsecase) *CartService {
return &CartService{usecase: u}
}

wire.go的文件内容

1
2
3
4
5
6
7
8
9
10
11
package example

import "github.com/google/wire"

func GetCartService() *CartService {
panic(wire.Build(
NewCartService,
NewCartUsecase,
NewCartRepo,
))
}

执行wire . 之后可以生成的函数和我们自己手写其实差不多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package example

// Injectors from wire.go:

func GetCartService() *CartService {
iCartRepo := NewCartRepo()
iCartUsecase := NewCartUsecase(iCartRepo)
cartService := NewCartService(iCartUsecase)
return cartService
}

How wire works

Wire 主要受 Java 的Dagger 2启发,使用代码生成而不是反射或Service Loader,主要有如下几个优点:

  • 方便 debug,若有依赖缺失编译时会报错
  • 因为不需要 Service Locators, 所以对命名没有特殊要求
  • 避免依赖膨胀。生成的代码只包含被依赖的代码,而运行时依赖注入则无法作到这一点
  • 依赖关系静态存于源码之中, 便于工具分析与可视化

参考资料:

  1. GitHub - uber-go/dig: A reflection based dependency injection toolkit for Go
  2. GitHub - google/wire: Compile-time Dependency Injection for Go
  3. https://medium.com/@dche423/master-wire-cn-d57de86caa1b
  4. https://go.dev/blog/wire

QUIC 协议简介

QUIC英文全称为Quick UDP Internet Connections,意思是快速UDP网络连接,是一种位于传输层的网络传输协议,最早由Google开发,旨在使用UDP来替换TCP协议,解决TCP传输建连和传输延迟并且提供类似SSL/TLS网络安全功能,希望使用该协议使得网络传输更快。

2018 年 10 月,互联网工程任务组 HTTP 及 QUIC 工作小组正式将基于 QUIC 协议的 HTTP (英语:HTTP over QUIC) 重命名为 HTTP/3 以为确立下一代规范做准备。

QUIC协议主要特点

  • 减小连接创建时的开销,QUIC使协商密钥和支持的协议成为初始握手过程的一部分,当客户端打开连接时,服务器响应的数据包包括将来的数据包加密所需的数据
  • QUIC的另一个目标是提高网络切换期间的性能,例如当移动设备的用户从WiFi热点切换到移动网络时发生的情况。
  • QUIC在应用程序空间中实现,而不是在操作系统内核中实现。QUIC允许更容易地进行未来更改,因为它不需要更改内核就可以进行更新
  • 改进的拥塞控制
  • 避免队头阻塞的多路复用
  • 前向冗余纠错

协议提出历史背景

  1. 多个协议栈在内核中实现,升级困难问题

当前互联网中最常用的应用层协议HTTP,传输层协议采用TCP,安全套接字采用SSL/TLS,域名解析解析采用DNS,网络层协议采用IPV4协议。这几种协议历史悠久,更新缓慢,由于很多协议栈的实现在内核中实现,互联网中间设备众多,协议更新难度大。国家层面也在推进IPV6基础建设,但是由于难度大至今仍未普及。

  1. 建立连接握手延迟问题

当前互联网中使用最多的HTTP协议,HTTP/1.1、HTTPS、HTTP2,都使用了TCP传输层协议,HTTPS和HTTP2才使用到了SSL/TLS,这就出现了多个建连延迟。一个TCP三次握手延迟,另一个是TLS建连至少需要2个RTT。

  1. TCP队头阻塞问题

TCP使用了序号来标识报文,数据处理必须按照顺序处理,如果前面的数据丢失,后面的数据到了也不会通知应用层处理。所以 QUIC 协议选择了 UDP,因为 UDP 本身没有连接的概念,不需要三次握手,优化了连接建立的握手延迟,同时在应用程序层面实现了 TCP 的可靠性,TLS 的安全性和 HTTP2 的并发性,只需要用户端和服务端的应用程序支持 QUIC 协议,完全避开了操作系统和中间设备的限制。(引用自科普:QUIC协议原理分析)

  1. TCP拥塞控制算法问题

传统的TCP拥塞控制算法,基本分为慢启动,拥塞避免,快速重传,快速恢复,后续改进的拥塞控制算法诸如CUBIC,BBRReno等,这些拥塞控制算法实现不太一样,但是可以确定的是这些算法都需要内核支持。

Quic协议优势

  1. 不是在内核中实现

QUIC在应用程序空间中实现,而不是在操作系统内核中实现。QUIC允许更容易地进行未来更改,因为它不需要更改内核就可以进行更新

  1. 建连握手延迟低

Quic协议可在1RTT或者0RTT就可以完成服务器端和移动端的建连

  1. 避免队投阻塞

  2. 改进的拥塞控制

QUIC协议也实现了BBR/CUBIC等控制算法,不过是在应用层实现的这些控制算法,并非在操作系统的内核层面。最为牛逼的还是QUIC协议可以同时使用多个拥塞控制算法,甚至可以为每个接入用户单独设置拥塞控制算法,如果再加上人工智能算法,根据终端用户接入的网络环境,自动设置不同的控制算法,这还是非常牛逼

HTTP2协议-概览

HTTP/1.1 自从 1997 年发布以来,我们已经使用 HTTP/1.x 相当长一段时间了,但是随着近十年互联网的爆炸式发展,从当初网页内容以文本为主, 到现在以富媒体(如图片、声音、视频)为主, 而且对页面内容实时性要求高的应用越来越多 (如聊天、视频直播), 于是当时协议规定的某些特性,已经无法满足现代网络的需求了。

HTTP1.1 协议缺陷

  1. 请求响应延迟问题

HTTP1.0协议模型只支持请求-响应,所有的请求和响应是串行的,也就是说HTTP协议客户端发送一个请求必须要等到服务端响应之后才能再发下一个请求,并且每次请求时必须重新创建TCP连接,这种通信模型效率很低,根本无法满足通信需求。

HTTP1.1增加了Keep-alive机制和Pipelining机制,前者主要解决每次HTTP请求必须要创建TCP连接的问题;后者主要解决请求-响应串行问题。

Read more »

Golang 编程规范

本文转发自:Uber Go语言编码规范

Uber是世界领先的生活出行服务提供商,也是Go语言的早期adopter,根据Uber工程博客的内容,大致可以判断出Go语言在Uber内部扮演了十分重要的角色。Uber内部的Go语言工程实践也是硕果累累,有大量Go实现的内部工具被Uber开源到github上,诸如被Gopher圈熟知的zapjaeger等。2018年年末Uber将内部的Go风格规范开源到github,经过一年的积累和更新,该规范已经初具规模,并受到广大Gopher的关注。本文是该规范的中文版本,并”夹带“了部分笔者的点评,希望对国内Gopher有所帮助。

注:该版本基于commit 3baa2bd翻译,后续不会持续更新。

img{512x368}

Read more »

Log Structured Merge Trees 概述

ClickHouse数据存储原理采用的思想和LSM基本一致,本文主要讲述LSM数据存储的设计思想和基本原理。本文部分内容来自网上一篇译文,原文地址已找不到。

具体十年前,谷歌发表了 BigTable”的论文,提出了一种数据存储的方法叫Log Structured-Merge Tree,简称LSM。现在LSM已被用在许多产品的文件结构策略:HBase、 Cassandra、 LevelDB、RocksDB,包括刚开源不久的ClickHouse数据库的存储引擎也采用的是类似LSM原理。

背景知识

简单地说,LSM的设计主要是为了解决随机写的效率问题而采用的顺序写,可大大提升数据读写性能而提出的。当前大家都知道磁盘随机读写速度慢,顺序写可大大提升读写吞吐量。这种采用顺序写而提升系统吞吐量比较出名的开源组件是消息中间件Kafka,Kafka采用offset + 追加写具备很高的读写性能,也是目前大数据平台必备组件之一。

但是对于数据存储系统,采用顺序追加写可解决写性能问题,对于读主要有几种设计方向:

  1. 二分查找:将文件数据有序保存,使用二分查找来完成特定key的查找。
  2. 哈希:用哈希将数据分割为不同的bucket
  3. B+树:使用B+树 或者 ISAM 等方法,可以减少外部文件的读取
  4. 外部文件: 将数据保存为日志,并创建一个hash或者查找树映射相应的文件。

所有的方法都可以有效的提高了读操作的性能(最少提供了O(log(n)) ),但是,却丢失了日志的写性能。上面这些方法,都强加了总体的结构信息在数据上,数据被按照特定的方式放置,所以可以很快的找到特定的数据,但是却对写操作不友善,让写操作性能下降。更糟糕的是,当我们需要更新hash或者B+树的结构时,需要同时更新文件系统中特定的部分,这就是上面说的比较慢的随机读写操作。这种随机的操作要尽量减少

所以这就是 LSM 被设计的原因, LSM 使用一种不同于上述四种的方法,保持了日志文件写性能,以及微小的读操作性能损失。本质上就是让所有的操作顺序化,而不是像散弹枪一样随机读写。

Read more »

优化前提

  • 基础功能

  • 架构设计

  • 硬件资源

基本思路

​ 思想同c/c++系统优化基本相同

  • CPU密集型/IO密集型

    先分析程序是属于CPU密集型还是IO密集,如果是IO密集型,数据读写是否可使用内存盘、固态盘? 如果是CPU密集型,单线程程序是否可以改造成多线程? 多线程是否存在抢锁,或者同步? 是否可以减小锁粒度设置实现无锁设计

  • 函数高频调用

    高频调用的函数性能能有所提升,基本上可直接提升程序的整体性能

  • 锁粒度

    对于高性能计算程序,最好不要设计多线程抢锁的程序架构,如果锁设计不可避免那是否可以优化到减小锁粒度?

  • 线程池/协程池

    如果程序中存在大量创建线程的情况,可加一个线程池,减小创建线程的消耗在一定程序上可提高系统吞吐量。在Go中虽然创建协程开销很小,但是系统创建大量程序可能奔溃

  • 对象池

    如果程序中不可避免的创建大量小对象,可用对象池来减小GC压力,类似内存池

Read more »

ClickHouse 引擎

Clickhouse 提供了丰富的存储引擎,存储引擎的类型决定了数据如何存放、如何做备份、如何被检索、是否使用索引。不同的存储引擎在数据写入/检索方面做平衡,以满足不同业务需求。

Clickhouse 提供了十多种引擎,主要是用的是MergeTree 系表引擎。

TinyLog

这是最简单的表引擎,它将数据存储在磁盘上。每列都存储在一个单独的压缩文件中。写入时,数据被附加到文件的末尾。该类型引擎不支持索引 这种引擎没有并发数据访问控制:

  • 同时对一张表进行读写操作,读操会错误
  • 同时在多个查询中进行写入操作,数据将被破坏

使用此表的典型方法是一次写入:只需要一次写入数据,然后根据需要多次读取它。查询在单个流处理中执行,换句话说该引擎适用于相对较小的表格(官方推荐建议一百万行以内)。如果你有很多小表,使用这个表引擎是很有意义的,因为他比Log Engines(另一个引擎下边会介绍)更简单(需要代开的文件更少)。当你有大量读写效率很低的小表时,而且在与另一个DBMS一起工作时已经被使用了,你可能会发现切换到使用TinyLog类型的表更容易。 在Yandex.Metrica中,TinyLog表用于小批量处理的中间数据。

1
2
3
CREATE TABLE test.tinyLog_test ( id String,  name String) ENGINE = TinyLog

insert into test.Log_test (id, name) values ('1', 'first');

找到数据目录({home}/clickhouse/data/data/test)数据在磁盘上的结构如下

1
2
3
4
5
[root@root test]$ tree  -CL 5 ./tinyLog_test/
./tinyLog_test/
├── id.bin
├── name.bin
└── sizes.json

a.bin 和 b.bin 是压缩过的对应的列的数据, sizes.json 中记录了每个 *.bin 文件的大小:

1
2
[root@root test]$ cat ./tinyLog_test/sizes.json 
{"yandex":{"id%2Ebin":{"size":"28"},"name%2Ebin":{"size":"32"}}}
Read more »

TCMalloc

TCMalloc 是Google推出的内存管理库,比较常见的内存池库还有ptmalloc和jemalloc,相比后两者tcmalloc性能更改好,更适用于多线程高并发的场景。Golang 内存管理采用的方法和TCMalloc有点类似。

相比glibc maclloc,tcmalloc更快,根据官方提供的数据,glibc 2.3 malloc在同样的机器上maclloc/free大于需要300纳秒,tcmaclloc的实现只要50纳秒,两者相比差异还是挺大。所以开发者通常会基于glibc malloc峰值一个内存池,程序在启动的时候先向系统申请一大块内存,然后再把申请的大块内存划分为多个空闲列表。tcmalloc就是一个实现了这样的内存管理库。

简单来说,TCMalloc内存分配策略采用分级策略,一个是线程私有内存池,另一个是全局内存池。对于一些小内存的分配则直接在线程私有内存池中分配,无需加锁,大大地减小内存分配锁竞争,只有在线程私有内存不够时则从全局内存池分配。对于大容量的内存分配则直接从全局内存池中分配,这个时候则需要加锁。内存池的组织形式采用数组 + 链表的方式,数组的每个元素是一个链表,链表中每个元素大小则相同。

TCMalloc为每个线程分配一个线程本地缓存。线程本地缓存满足小分配。根据需要将对象从中央数据结构移动到线程本地缓存中,并使用定期垃圾回收将内存从线程本地缓存迁移回中心数据结构中

TCMalloc将大小小于等于32K的对象(“小”对象)与大对象区别对待。使用页面级分配器(页面是内存的4K对齐区域)直接从中央堆分配大对象。即,大对象始终是页面对齐的,并且占据整数页。

tcmalloc总览

小对象分配

每个线程会包含一个数组链表,同一个链表中拥有相同大小的空闲对象。

当分配一个小对象时,主要分配的步骤如下:

  • 根据要分配对象的大小,映射到相应的大小类
  • 在线程私有内存池中相应大小类的空闲链表
  • 如果空闲链表不为空,则返回第一个空闲对象,不需要加锁
  • 如果空闲链表为空,则需要从全局内存池中获取空闲对象

tcmalloc总览

大对象分配

对于大于32K的大对象分配则由全局内存来分配。全局内存的组织也是单链表数组,数组长度为256,分别对用1 page大小, 2 page大小(1 page=4k)

tcmalloc总览

参考文档:

  1. 官网文档
  2. http://legendtkl.com/2015/12/11/go-memory/#comments