前言

在上文中,我成功从MySQL到MongDB的转型,接下来,我将尝试将SpringBoot转为go


Protocol

首先,我需要让protoc即生成java代码,也生成go代码。

只需要指定 go_package 即可:

1
2
syntax = "proto3";
option go_package = "test/";

由于我还不是很了解生成的机制,所以这里的包路径全都一样的,你可以尝试对包路径分类。由于我这里有对别的proto文件有引用,导致go的引包错误了,所以才出此下策,当然,所有的名称在声名时也保证了一定不重复。

由于java的代码是通过maven插件生成的,这里我采用原本的protoc命令执行生成go代码,所以我写了这个脚本:

1
2
3
4
protoc --go_out=./ --go-grpc_out=require_unimplemented_servers=false:./ --proto_path=src/main/proto src/main/proto/common/*.proto
protoc --go_out=./ --go-grpc_out=require_unimplemented_servers=false:./ --proto_path=src/main/proto src/main/proto/service/entity/*.proto
protoc --go_out=./ --go-grpc_out=require_unimplemented_servers=false:./ --proto_path=src/main/proto src/main/proto/service/*.proto
protoc --go_out=./ --go-grpc_out=require_unimplemented_servers=false:./ --proto_path=src/main/proto src/main/proto/spider/*.proto

protoc: Protocol Buffers 编译器的命令行工具。

--go_out=./: 表示生成 Go 语言的 Protocol Buffers 代码,并将其输出到当前目录(生成的代码文件将与.proto文件位于相同的目录中)。

--go-grpc_out=require_unimplemented_servers=false:./: 表示同时生成 gRPC 代码,并将其输出到当前目录。参数 require_unimplemented_servers=false 意味着生成的 gRPC 代码不会强制实现所有服务接口,这在开发过程中很有用。

--proto_path=src/main/proto: 指定了存放.proto文件的路径,该路径为 src/main/proto

src/main/proto/common/*.proto: 指定要编译的.proto文件的位置和模式。例如,此处使用了通配符common/*.proto表示编译 common 目录下的所有.proto文件。

所以执行此脚本,生成代码,并上传至git仓库即可


Go项目创建

创建项目网上都有,这里建议从GitHub找一个crud练手一下,就基本了解go的语法了。此处我想记录的是,如何引入我们上一章节生成的go代码。

一般而言,我们引入一个项目是通过 go get github.com/xxxx/xxxx-proto 命令来拉取的,但是,假如我们的项目是私有GitLab的,如果不指定goproxy,它会把这个项目认为是公有项目,导致无法拉取,所以第一步是将这个地址仓库设为私有仓库,可以看这篇文章:Goland GitLab import 的步骤 | Leopold’s Blog (leofitz7.com)

当你成功 go get 后,就可以在项目中使用protoc生成的代码了。

接下来我们看一下主函数:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
"gitee.com/xxx/xxx-proto/test"
"google.golang.org/grpc"
"leopold-test/src/conf"
"leopold-test/src/service"
"leopold-test/src/util"
"log"
"net"
"net/http"
"os"
)

func main() {

// init godotenv用于环境变量、logger用于日志、redis缓存
conf.Init()

// 创建net
lis, err := net.Listen("tcp", ":"+os.Getenv("GRPC_PORT"))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}

// 创建grpc server
server := grpc.NewServer()

// 微服务注册
test.RegisterV0TestSpiderServiceServer(server, &service.V0TestSpiderServiceImpl{})

// 健康检查
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
})

// 协程启动http心跳
go func() {
err := http.ListenAndServe(":"+os.Getenv("HTTP_PORT"), nil)
if err != nil {
util.Log().Error("心跳失败 -> %v", err)
}
}()

// 启动grpc
if err := server.Serve(lis); err != nil {
util.Log().Error("grpc启动失败 -> %v", err)
}
}

对于服务端的实现,我们只需要实现 V0TestSpiderServiceImpl 即可:

1
2
3
4
5
6
7
type V0testSpiderServiceImpl struct {
}

func (c *V0testSpiderServiceImpl) Submit(ctx context.Context, req *test.SubmitReq) (*test.SubmitResp, error) {
// 核心实现
return &test.SubmitResp{}, nil
}

对于客户端的使用,我们只需要创建连接,并执行方法即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var kacp = keepalive.ClientParameters{
Time: 60 * time.Second, // send pings every 60 seconds if there is no activity
Timeout: 1 * time.Second, // wait 1 second for ping ack before considering the connection dead
PermitWithoutStream: false, // 如果您不需要在没有活动流的情况下发送ping请求,则可以将 keepalive.ClientParameters.PermitWithoutStream 的值设置为false。这样做可以防止客户端发送过多的ping请求。
}

// 创建context
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()

// 建立grpc连接
conn, err := grpc.DialContext(ctx, os.Getenv("test_SERVICE_ADDR"), grpc.WithInsecure(), grpc.WithKeepaliveParams(kacp))

// grpc调用Test方法
_, err = test.NewV0testServiceResourceServiceClient(dial).Test(context.Background(), &test.ServiceResourceSaveLiveReq{
k: "我要给服务端发条消息"
})

至此,我们的代码就写好了


Dockerfile

接下来我们打包镜像:

1
2
3
4
5
6
7
8
9
10
FROM alpine:3.17
RUN echo "http://mirrors.aliyun.com/alpine/v3.17/main/" > /etc/apk/repositories \
&& echo "http://mirrors.aliyun.com/alpine/v3.17/community/" >> /etc/apk/repositories \
&& apk update && apk add --no-cache tzdata \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
ENV LANG=C.UTF-8
COPY main .env /
EXPOSE 9090
CMD ["/main"]

注意,这里我没有像网上那样,引入goland sdk,go build 编译,再copy二进制程序,因为我们的二进制也就20MB,没必要把整个编译SDK打包进去,我们提前打好即可,只需要拷贝二进制程序,并执行即可。但请注意:

  1. 对于 alpine 镜像,如果想执行二进制程序,需要指定 go build --tags netgo,因为 alpine 太小了,缺少了很多包。netgo标记告诉Go编译器只使用纯Go实现的网络库,而不依赖于操作系统提供的网络功能。它会禁用对cgo(C语言调用Go函数)的使用,以及对操作系统特定的网络库的依赖。这样可以确保生成的二进制文件在不同操作系统上都能够独立运行,而无需依赖外部动态链接库。通过使用--tags netgo,可以确保构建的二进制文件不依赖于底层操作系统的网络库,从而增加了可移植性和可靠性。这在某些需要在不同平台上分发的应用程序或服务中非常有用。

  2. 对于 alpine 镜像,有时候我们恰好需要gnu libc的依赖,这时候就需要慎重考虑是否一定需要使用Alpine镜像,因为Alpine镜像的小巧正是建立在busybox和musl libc基础之上的。因为我这个项目的爬虫程序需要chrome安装,需要gnu libc依赖,所以我是用了三方镜像 frolvlad/alpine-glibc:alpine-3.17_glibc-2.34,所以对于不同的项目,镜像的选择也很重要。


测试内存占用

通过 kubectl top pods pod名称 -n 命名空间 来查看内存占用情况:

1
2
3
4
root@m1:~# kubectl top pods -n test
NAME CPU(cores) MEMORY(bytes)
go-xxx 1m 3Mi
java-xxx 1m 240Mi

可以看到,go项目不需要加载jvm,内存非常的小,所以这个服务将分配更多的cpu,发挥协程的特性,爬虫性能大幅提升


其他

对于go爬虫而言,静态页面可使用colly,动态页面可使用chromedp,这里就不多叙述了,坑也不少。

接下来,我可能会学习一段go语言的使用,对于协程、锁的运用还得了解。在下个月中,我将尝试创建pulsar消息队列,并引入分布式锁,将所有服务按照领域事件驱动,这样我们的微服务就基本有了一个小的生态了,下个月见~~