RocketMQ 设计与理解 置顶! 有更新!

整体架构

rocketmqarchitecture1.png

RocketMQ 主要由 Producer、Broker、Consumer、Name Server 四个部分组成。

其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息,Name server 充当路由消息的提供者。

每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。

image-20210402173106318

Topic

Topic 是一种逻辑上的分区,是同一类消息的集合,每一个消息只能属于一个 Topic ,是RocketMQ进行消息订阅的基本单位。

每个 topic 会被分成很多 Messsage Queue ,和 Kafka 中的 Partition 概念一样,topic 的数据被分布在不同的 Message Queue 中。

在业务增长,消息量增大时,可以增大 topic 的 Message Queue,这样可以将压力分摊到更多的 broker 上。因为 Producer 可以发送消息的时候可以通过指定的算法,将消息均匀的发送到每个 Message Queue。

阅读全文 »

开源 gev: Go 实现基于 Reactor 模式的非阻塞 TCP 网络库 置顶! 有更新!

gev 轻量、快速的 Golang 网络库

gev 是一个轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库,底层并不使用 golang net 库,而是使用 epoll 和 kqueue,因此它并不支持 Windows。

为什么有 gev

Golang 的 goroutine 虽然非常轻量,但是每启动一个 goroutine 仍需要 4k 左右的内存。读了鸟窝大佬的文章【百万 Go TCP 连接的思考: epoll方式减少资源占用】后,便去研究了了下 evio

evio 虽然非常快,但是仍然存在一些问题,便尝试去优化它,于是有了 eviop 项目。关于 evio 的问题可以看我的另一篇博文 【Golang 网络库evio一些问题/bug和思考】。在优化 evio 完成 eviop 的过程中,因为其网络模型的缘故,愈加感觉修改它非常麻烦,成本比重新搞一个还高。

最终决定自己重搞一个,更加轻量,不需要的全去掉。加上大学时学习过 muduo ,便参考 muduo 的使用的 Reactor 模型实现 gev 。

在 linux 环境下,gev 底层使用 epoll ,这是 gev 会专注优化的地方。在 mac 下底层使用 kqueue,可能不会过多关注这部分的优化,毕竟很少有用 mac 做服务器的(Windows 环境"暂"不支持)。

特点

  • 基于 epoll 和 kqueue 实现的高性能事件循环
  • 支持多核多线程
  • 动态扩容 Ring Buffer 实现的读写缓冲区
  • 异步读写
  • SO_REUSEPORT 端口重用支持
阅读全文 »

Go 网络库并发吞吐量测试 置顶!

https://github.com/Allenxuxu/gev

本文主要测试 gev 网络库和其他三方 Go 网络库以及标准库的吞吐量对比。

测试对象

  • gev :一个轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库
  • eviop :evio 的优化版本
  • evio :Fast event-loop networking for Go
  • gnet :eviop 的网络模型替换版本
  • net 标准库

测试方法

采用陈硕测试 muduo 使用的 ping pong 协议来测试吞吐量。

简单地说,ping pong 协议是客户端和服务器都实现 echo 协议。当 TCP 连接建立时,客户端向服务器发送一些数据,服务器会 echo 回这些数据,然后客户端再 echo 回服务器。这些数据就会像乒乓球一样在客户端和服务器之间来回传送,直到有一方断开连接为止。这是用来测试吞吐量的常用办法。

测试的客户端代码: https://github.com/Allenxuxu/gev/blob/master/benchmarks/client/main.go

测试脚本:https://github.com/Allenxuxu/gev/blob/master/benchmarks/bench-pingpong.sh

主要做两项测试:

  • 单线程单个 work 协程测试,测试并发连接数为 10/100/1000/10000 时的吞吐量
  • 4线程4个 work 协程测试,测试并发连接数为 10/100/1000/10000 时的吞吐量

所有测试中,ping pong 消息的大小均为 4096 bytes,客户端始终是4线程运行。

测试结果

gev11.png

gev44.png

阅读全文 »

Go1.16 embed 和 Vue

我们再 static 目录里增加一个 go 文件,这里使用 1.16 的 embed 来嵌入当前目录的静态文件:

package static
​
import "embed"
​
//go:embed index.html favicon.ico css img js
var Static embed.FS
​
阅读全文 »

Golang slice map channel 小技巧

Slice vs Array

Slice 和 Array 是不同的类型

package main
​
func main() {
   s := make([]int, 100)
   printSlice(s)
​
   var a [100]int
   printArray(a)
}
​
func printSlice(s []int) {
   println(len(s)) // 100
   println(cap(s)) // 100
}
​
func printArray(a [100]int) {
   println(len(a)) // 100
   println(cap(a)) // 100
}

Slice 结构体

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

下面的汇编表明,当类型是 slice 的时候,打印 len 或者 cap 的时候,会去栈上取数据:

MOVQ 0x28(SP), AX       
  MOVQ AX, 0x8(SP)        
  CALL 0xbfc              [1:5]R_CALL:runtime.printlock<1>        
  MOVQ 0x8(SP), AX 
  MOVQ AX, 0(SP)

**而当类型是 array 时候,直接用的 0x64 (10进制 100): **MOVQ $0x64, 0(SP)

查看具体汇编代码

go tool compile -N -l main.go
go tool objdump main.o

Slice 的自动扩容

Slice 可以使用 append 函数新增数据,当容量不足的时候,会自动新申请一块空间,将原有数据复制过去,再新增数据。

阅读全文 »

2019 总结--告别 有更新!

告别南京

2019 年 6 月告别南京,告别校园,我来到了上海。

与大多数人相同,初入社会,无比怀恋校园生活。

后悔大学四年,都没有去逛过总统府,没有去爬过紫金山,没有在操场多停留。

感谢那四年宿舍、实验室两点一线的我,让我在在工作中未曾感到过压力。

但是,南京,终究是回不去了。

成长

2019 前半年,3 月份的时候,我就提前来了公司实习。一直到差不多五月初,我才回了学校。

这段时间的记忆并不深刻,几乎想不起来做了什么,算是体验了一下职场生活吧。

工作后一段时间,才开始真正使用 Github 。

2019 主要输出了三个半项目吧

  • 🚀一个轻量、快速的基于 Reactor 模式的非阻塞 Golang 网络库,支持自定义协议,轻松快速搭建高性能服务器。
    https://github.com/Allenxuxu/gev
阅读全文 »

golang protobuf 字段为零值时 json 序列化忽略问题

protoc 编译生成的 pb.go 文件,默认情况下 tag 中会设置 json 忽略零值的返回属性 omitempty

type Message struct {
	Header               map[string]string `protobuf:"bytes,1,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
	Body                 []byte            `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"`
	XXX_NoUnkeyedLiteral struct{}          `json:"-"`
	XXX_unrecognized     []byte            `json:"-"`
	XXX_sizecache        int32             `json:"-"`
}

一个比较 hack 的方式,是在 pb.go 文件生成后,手动去删掉 omitempty 。每次手动去删除,比较麻烦且容易出错,下面提供一个 Makefile ,每次生成 pb.go 的时候就去删除 omitempty

阅读全文 »

二叉堆与堆排序 有更新!

二叉堆是一组能够用堆有序的完全二叉树排序的元素,一般用数组来存储。

大顶堆, 每个结点的值都大于或等于其左右孩子结点的值,其顶部为最大值。

小顶堆,每个结点的值都小于或等于其左右孩子结点的值,其顶部为最小值。

二叉堆

性质

  • 根节点在数组中的位置是 1
    • 左边子节点 2i
    • 右子节点 2i+1
    • 父节点 i / 2
    • 最后一个非叶子节点为 len / 2
  • 根节点在数组中的位置是 0
  • 左子节点 2i + 1
  • 右边子节点 2i+ 2
  • 父节点的下标是 (i − 1) / 2
  • 最后一个非叶子节点为 len / 2 - 1

实现

构造二叉堆

  • 找到最后一个非叶子节点 ( len / 2 或者 len / 2 - 1)
  • 从最后一个非叶子节点下标索引开始递减,逐个下沉

插入节点

  • 在数组的最末尾插入新节点
  • 将最后一个节点上浮,时间复杂度为O(log n)
    • 比较当前节点与父节点
    • 不满足 堆性质* *则交换
阅读全文 »

【CI/CD 实践】 Github Actions 配置 CI/CD 自动发布 docker 镜像 有更新!

Github Actions 是 Github 内置的 CI/CD 工具,现在已经对所有的开源项目免费开放了。

本文主要记录使用 Github Actions 实践 CI/CD 的一些配置。

功能目标

  • 代码静态检查
  • 代码单元测试
  • release/tag 时自动 build 镜像并推送到 docker hub

项目 Dockerfile 和 Makefile

项目主要目录

image.png

image.png

阅读全文 »

go-micro 动态加载插件源码分析 有更新!

go-micro 框架支持动态加载插件,无需修改代码。

源码分析

启动服务前,设定 MICRO_PLUGIN 环境变量指定 .so 文件路径,支持多个插件,逗号分割。程序启动前会读取 MICRO_PLUGIN 环境变量,并完成插件设定。

下面是其内部实现:

go-micro/service.go

从上面的代码可以看出,service 初始化化的时候,读取 MICRO_PLUGIN 环境变量中指定的 .so 文件路径。并且调用 plugin 包,逐个 Init

下面我们看下 plugin 包的实现:

阅读全文 »

XConf : Golang 构建的开源分布式配置中心 有更新!

Github项目地址 | 线上demo

微服务架构愈演愈烈,但是社区一直缺少一个部署简便,高可用的配置中心。
XConf 是一个基于 go-micro 微服务框架构建的分布式配置中心,提供配置的管理与发布、实时推送。

design.png

配置中心底层存储采用 MySQL 数据库,主要分为三个服务:

  • config-srv : 负责底层配置的读写
  • admin-api : 负责与管理页面交互,相关鉴权,账号体系也会与此模块交互
  • agent-api : 负责与客户端交互,提供配置读取和推送。

配置中心本身就是一个“读多写少”的服务,所以在 agent-api 服务中增加缓存,从而有效增加并发性能。得益于 go-micro 框架,三个服务可以便捷的横向伸缩。例如,当获取配置的客户端较多时,可以增加 agent-api 实例。

配置获取采用 HTTP 方式实现,配置的实时推送采用 HTTP Long Polling(长轮询)方式实现。选择 HTTP 方式,更加便于各种语言接入配置中心。

Golang 语言读取配置和监听实时配置推送( ➡️ 源码地址 ):

阅读全文 »

[ gev ] Go 语言优雅处理 TCP “粘包” 有更新!

https://github.com/Allenxuxu/gev

gev 是一个轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库,支持自定义协议,轻松快速搭建高性能服务器。

最近对 gev 进行了一次较大改动,主要是为了能够以插件的形式支持各种自定义的数据协议,让使用者可以便捷处理 TCP 粘包问题,专注于业务逻辑。

做法如下,定义一个接口 Protocol

// Protocol 自定义协议编解码接口
type Protocol interface {
	UnPacket(c *Connection, buffer *ringbuffer.RingBuffer) (interface{}, []byte)
	Packet(c *Connection, data []byte) []byte
}

用户只需实现这个接口,并注册到 server 中,当客户端数据到来时,gev 会首先调用 UnPacket 方法,如果缓冲区中的数据足够组成一帧,则将数据解包,并返回真正的用户数据,然后在回调 OnMessage 函数并将数据通过参数传递。

下面,我们实现一个简单的自定义协议插件,来启动一个 Server :

| 数据长度 n |  payload |
|  4字节    |  n 字节   |
阅读全文 »

Uber Go 风格指南(译) 有更新!

简介

风格是指规范代码的共同约定。风格一词其实是有点用词不当的,因为共同约定的范畴远远不止 gofmt 所做的源代码格式化这些。

本指南旨在通过详尽描述 Uber 在编写 Go 代码中的注意事项(规定)来解释其中复杂之处。制定这些注意事项(规定)是为了提高代码可维护性同时也让工程师们高效的使用 Go 的特性。

这份指南最初由 Prashant Varanasi 和 Simon Newton 编写,目的是让一些同事快速上手 Go 。多年来,已经根据其他人的反馈不断修改。

这份文档记录了我们在 Uber 遵守的 Go 惯用准则。其中很多准则是 Go 的通用准则,其他方面依赖于外部资源:

  1. Effective Go
  2. The Go common mistakes guide

所有的代码都应该通过 golintgo vet 检查。我们建议您设置编辑器:

  • 保存时自动运行 goimports
  • 自动运行 golintgo vet 来检查错误

您可以在这找到关于编辑器设定 Go tools 的相关信息:

https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

阅读全文 »

Golang 网络库 evio 一些问题/bug和思考 有更新!

Fast event-loop networking for Go

最近翻了 evio 的源码,发现一些问题,主要集中在 linux 平台 epoll 上和读写的处理。

  • 用来唤醒 epoll 的 eventfd 写入数据没有读出
  • listen 的 fd 注册到所有事件循环,epoll 的惊群问题
  • loopWrite 在内核缓冲区满,无法一次写入时,出现写入数据丢失
阅读全文 »

Golang 高性能网络库 evio 源码解析 有更新!

阅读前提:了解 epoll

evio 是一个基于事件驱动的网络框架,它非常轻量而且相比 Go net 标准库更快。其底层使用epoll 和 kqueue 系统调度实现。

echo.png


原理

evio 是 Reactor 模式的简单实现。Reactor 本质就是“non-blocking IO + IO multiplexing”,通过非阻塞IO+ IO 多路复用来处理并发。程序运行一个或者多个事件循环,通过在事件循环中注册回调的方式实现业务逻辑。

evio 将所有文件描述符设为非阻塞,并注册到事件循环( epoll / kqueue )中。相较于传统的 per thread per connection 的处理方法,线程使用更少,线程资源利用率更高。

evio 需要在服务启动前,注册回调函数,当事件循环中有事件到来时,会调用回调函数处理。

使用示例

先从一个简单的 echo server 的例子来了解 evio 。

阅读全文 »

Go net/http 浅析

使用标准库构建 HTTP 服务

Go 语言标准库自带一个完善的 net/http 包,可以很方便编写一个可以直接运行的 Web 服务。

package main

import (
    "log"
    "net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
    log.Println(r.Method, r.Host, r.RequestURI)
    w.Write([]byte("hello"))
}

func main() {
	http.HandleFunc("/hello", hello)            //设置访问的路由
	// http.Handle("/hello", http.HandlerFunc(hello)) // 和上面写法等价

    err := http.ListenAndServe(":9090", nil)    //设置监听的端口并启动 HTTP 服务
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}
$  curl -v 127.0.0.1:9090/hello
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 9090 (#0)
> GET /hello HTTP/1.1
> Host: 127.0.0.1:9090
> User-Agent: curl/7.54.0
> Accept: */*
>  
< HTTP/1.1 200 OK
< Date: Tue, 10 Sep 2019 10:52:07 GMT
< Content-Length: 5
< Content-Type: text/plain; charset=utf-8
<  
* Connection #0 to host 127.0.0.1 left intact
hello

上面短短几行代码,已经启动了一个 HTTP 服务。 在浏览输入 127.0.0.1:9090/hello 或者执行 curl -v 127.0.0.1:9090/hello 可以验证。

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

http.HandleFunc("/hello", hello) 会在 net/http 的默认路由中注册 hello 处理函数,这也是我们为什么在 http.ListenAndServe(":9090", nil) 中传入 nil,传入 nil 意味着使用默认的路由器。

阅读全文 »