讲一下我在七牛云实习的前三个月的经历
由于我的资历尚浅,一开始其实我是很怕跟不上大家的,并且是自己完全不熟悉的方向(Java -> Go -> Go+/LLGo),但是很幸运,七牛提供的实训营,只要你在努力,有进步,导师们都看在眼里。导师们都是公司技术top并且很负责,我与队友也相处的很融洽,最终我也慢慢的重拾自信。
目标
一开始是产品调研和目标确定,我们的想法不断的被推倒或者认同,最后和队友以及导师们确定了最终的一些目标:
核心点就是将 Rust 生态的能力带到 LLGo 中来
首先是学会怎么将 Rust 库迁移到 LLGo
迁移一些 Rust 生态到 LLGo 中验证可行性
使用 Libuv 为 LLGo 实现异步 I/O 能力
使用 Hyper 来实现 LLGo 的 net/http 库
使用 tokio/io-uring 为 LLGo 加速异步 I/O 能力
Rust 到 LLGo 这条路之前 LLGo 的开发者并没有尝试过,所以一切都得我们自己探索,充满了未知性。
我们最终都在围绕着使用 Rust 生态的 Hyper 来实现 net/http 库,但是考虑到 LLGo 中的 goroutine 使用的是 pthread,所以我们才引入了 Libuv,主要依靠它的事件循环机制来提高我们处理请求的速度。
前置准备
探索怎么将 Rust 迁移到 LLGo
这里感兴趣的同学可以看一下我们写的文档:How to support a Rust Library
这里真的就是要感谢我们的 之阳同学,因为他有过迁移 C 到 LLGo 的经验,所以给予了我们很大的帮助,让我们能够少走很多的弯路。(第一次感叹实习的同学们和导师们真的特别友好!!!)
迁移Hyper & Libuv
迁移的过程是枯燥的,我们只需要根据文档,就能够迁移大部分内容,如果遇到难点,是可以马上找到导师进行探讨的(第二次感叹实习的同学们和导师们真的特别友好!!!)
我们可以看一下 Libuv 在迁移前(C),和迁移后(Go)代码的调用模样(伪代码):
C:
loop = uv_default_loop();
uv_tcp_t server;
uv_tcp_init(loop, &server);
struct sockaddr_in addr;
uv_ip4_addr("0.0.0.0", DEFAULT_PORT, &addr);
uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
int r = uv_listen((uv_stream_t*) &server, DEFAULT_BACKLOG, on_new_connection);
if (r) {
fprintf(stderr, "Listen error %s\n", uv_strerror(r));
return 1;
}
uv_run(loop, UV_RUN_DEFAULT);
Go:
loop := libuv.DefaultLoop()
var server libuv.Tcp
libuv.InitTcp(loop, &server)
var addr cnet.SockaddrIn
libuv.Ip4Addr(c.Str("0.0.0.0"), DEFAULT_PORT, &addr)
server.Bind((*cnet.Sockaddr)(unsafe.Pointer(&addr)), 0)
r := (*libuv.Stream)(unsafe.Pointer(&server)).Listen(DEFAULT_BACKLOG, onNewConnection)
if r != 0 {
fmt.Fprintf(os.Stderr, "Listen error %s\n", c.GoString(libuv.Strerror(libuv.Errno(r))))
return 1
}
loop.Run(libuv.RUN_DEFAULT)
官方Go编译器当然是不能运行的,得使用我们的 LLGo 编译器(崇拜🤩
实现 net/http 标准库
我负责实现的是 client 相关的功能,这里借用一下我队友的图来说明一下设计架构:
https://blog.cdn.hackerchai.com/images/2024/10/llgo-net-http-server.png
server 部分和 client 部分的设计基本一致,当用户发送一个请求,就将该请求分配给一个 LibuvLoop,利用 Libuv 事件循环促使 Hyper Executor 进行 Poll Task 操作完成对于 Request 的处理,然后我们得到 Response 对象并返回给用户。
为了加速性能和利用 CPU 多核特性,我们采用了 thread-per-core 的架构设计,一个 CPU 核心对应一个 Libuv 的 EventLoop 并且对应一个 Hyper 的 Executor,然后采用一定的 load balance 策略去分配进来的请求。
可以看一下我们的一个实现成果,我们正常调用 LLGo 的 net/http 库,就和使用标准库一样,但是底层实现逻辑已经被我们替换掉:
package main
import (
"fmt"
"io"
"github.com/goplus/llgoexamples/x/net/http" // 导入的包被替换
)
func main() {
resp, err := http.Get("https://httpbin.org")
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
fmt.Println(resp.Status, "read bytes: ", resp.ContentLength)
for key, values := range resp.Header {
for _, value := range values {
fmt.Printf("%s: %s\n", key, value)
}
}
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
下面是我们分配 libuv eventLoop 的逻辑,主要通过 hash 进行负载均衡,好处是复用 连接池 中的 空闲连接 时,使用的是同一个 eventLoop:
func (t *Transport) getClientEventLoop(req *Request) *clientEventLoop {
t.loopsMu.Lock()
defer t.loopsMu.Unlock()
if t.loops == nil {
t.loops = make([]*clientEventLoop, cpuCount)
}
key := t.getLoopKey(req)
h := fnv.New32a()
h.Write([]byte(key))
hashcode := h.Sum32()
return t.getOrInitClientEventLoop(hashcode % uint32(cpuCount))
}
在下面 Libuv 的 uv_check 的回调中,去执行 exec.Poll 得到 task,然后去执行我们的 task :
func readWriteLoop(checker *libuv.Check) {
eventLoop := (*clientEventLoop)((*libuv.Handle)(c.Pointer(checker)).GetData())
// The polling state machine! Poll all ready tasks and act on them...
task := eventLoop.exec.Poll()
for task != nil {
eventLoop.handleTask(task)
task = eventLoop.exec.Poll()
}
}
感谢
对我来说,这段实习实习真的很棒。我们小队从产品设计,到技术选型,再到内容实现,每一步都有着许多的困难,接下来做什么,怎么做都是一个问题,队员间也充斥着不同的声音。之前的我们更多的是参与到内容实现的部分,也更多的是一个人完成工作。而这样一次机会,让我能够看到整个产品的生命周期,也能够和队友一起去思考,去讨论,去实现。导师们也很友善,遇到技术难点,导师也会引导我们去确定方案。实训结束后,能够明显感觉到自己解决问题的能力提升了很多,知道如何去定位问题,知道如何去思考解决问题的方式,当然提升最大的我觉得还是团队的协作能力。在这里你就是主角,你可以充分的展示自己,同时也能够从导师和队友身上学到很多。
也很感谢我的导师们 傲飞老师 、 七叶老师 、 长军老师 还有 老许 的倾情指导,让我对于系统底层和 Golang 底层有了更深刻的理解。也要感谢以下我的几位给力队友:轶晟、之阳,如果没有他们我可能很难独自完成这么复杂的工作。(第三次感叹实习的同学们和导师们真的特别友好!!!)
链接
幻灯片: qiniu-campus-slide
LLGo:https://github.com/goplus/llgo
适配 Libuv 的 Hyper FFI 分支: feature/server-ffi-libuv-demo