修改用户服务代码
前面我们已经安装好了微服务的一些基础设施,现在我们需要开始编写微服务代码,构建容器,启动服务并将其注册到注册中心中。
更正引用错误
打开micro生成的用户服务代码模板的入口文件main.go
,我们发现因为我们修改了go.mod
文件所以导致一些引用失效,所以我们需要将这些文件的引用更正
修改main.go
package main
import (
"github.com/869413421/micro-service/user/handler"
"github.com/869413421/micro-service/user/subscriber"
"github.com/micro/go-micro/v2"
log "github.com/micro/go-micro/v2/logger"
proto "github.com/869413421/micro-service/user/proto/user"
)
func main() {
// New Service
service := micro.NewService(
micro.Name("micro.service.user"),
micro.Version("latest"),
)
// Initialise service
service.Init()
// Register Handler
proto.RegisterUserHandler(service.Server(), new(handler.User))
// Register Struct as Subscriber
micro.RegisterSubscriber("micro.service.user", service.Server(), new(subscriber.User))
// Run service
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
package handler
import (
"context"
"github.com/869413421/micro-service/user/proto/user"
log "github.com/micro/go-micro/v2/logger"
proto "github.com/869413421/micro-service/user/proto/user"
)
type User struct{}
// Call is a single request handler called via client.Call or the generated client code
func (e *User) Call(ctx context.Context, req *proto.Request, rsp *proto.Response) error {
log.Info("Received User.Call request")
rsp.Msg = "Hello " + req.Name
return nil
}
// Stream is a server side stream handler called via client.Stream or the generated client code
func (e *User) Stream(ctx context.Context, req *proto.StreamingRequest, stream proto.User_StreamStream) error {
log.Infof("Received User.Stream request with count: %d", req.Count)
for i := 0; i < int(req.Count); i++ {
log.Infof("Responding: %d", i)
if err := stream.Send(&user.StreamingResponse{
Count: int64(i),
}); err != nil {
return err
}
}
return nil
}
// PingPong is a bidirectional stream handler called via client.Stream or the generated client code
func (e *User) PingPong(ctx context.Context, stream proto.User_PingPongStream) error {
for {
req, err := stream.Recv()
if err != nil {
return err
}
log.Infof("Got ping %v", req.Stroke)
if err := stream.Send(&user.Pong{Stroke: req.Stroke}); err != nil {
return err
}
}
}
修改handler/user.go
package handler
import (
"context"
"github.com/869413421/micro-service/user/proto/user"
log "github.com/micro/go-micro/v2/logger"
proto "github.com/869413421/micro-service/user/proto/user"
)
type User struct{}
// Call is a single request handler called via client.Call or the generated client code
func (e *User) Call(ctx context.Context, req *proto.Request, rsp *proto.Response) error {
log.Info("Received User.Call request")
rsp.Msg = "Hello " + req.Name
return nil
}
// Stream is a server side stream handler called via client.Stream or the generated client code
func (e *User) Stream(ctx context.Context, req *proto.StreamingRequest, stream proto.User_StreamStream) error {
log.Infof("Received User.Stream request with count: %d", req.Count)
for i := 0; i < int(req.Count); i++ {
log.Infof("Responding: %d", i)
if err := stream.Send(&user.StreamingResponse{
Count: int64(i),
}); err != nil {
return err
}
}
return nil
}
// PingPong is a bidirectional stream handler called via client.Stream or the generated client code
func (e *User) PingPong(ctx context.Context, stream proto.User_PingPongStream) error {
for {
req, err := stream.Recv()
if err != nil {
return err
}
log.Infof("Got ping %v", req.Stroke)
if err := stream.Send(&user.Pong{Stroke: req.Stroke}); err != nil {
return err
}
}
}
修改subscriber/user.go
package subscriber
import (
"context"
log "github.com/micro/go-micro/v2/logger"
proto "github.com/869413421/micro-service/user/proto/user"
)
type User struct{}
func (e *User) Handle(ctx context.Context, msg *proto.Message) error {
log.Info("Handler Received message: ", msg.Say)
return nil
}
func Handler(ctx context.Context, msg *proto.Message) error {
log.Info("Function Received message: ", msg.Say)
return nil
}
至此,我们正常调整了代码。上面的代码只是作示例作为使用,后续会重构重新书写我们的业务,暂时不需要过于纠结。
测试代码是否能正常编译
执行go run main.go
可以看到我们的服务成功通过编译正常执行。
编写多阶段构建dockerfile
在微服务中我们正常编写好代码后,需要部署容器来运行服务,我们可以通过两种方式。
- 编写好dockerfile,编译好镜像,在docker-compose直接拉取部署
- 编写好dockerfile,通过docker-compose帮我们编译镜像运行
这里我们选择第二种方式
修改dockerfile
# user-service/Dockerfile
# 使用golang官方镜像,并命名为builder
FROM golang:1.13-alpine as builder
# 启用go Modules
ENV GO111MODULE on
# 安装git
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
apk update && \
apk add --no-cache git
# 设置工作目录
WORKDIR /app/micro-user-service
# 将目录中代码拷贝到镜像中
COPY . .
# 下载依赖,
RUN go env -w GOPROXY=https://mirrors.aliyun.com/goproxy,direct && go mod tidy
# 构建二进制文件,添加一些额外参数方便在alpin中运行它
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-w' -o micro-user-service ./main.go
# 第二阶段构造
FROM alpine:latest
# 更新依赖软件
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
apk update && \
apk add --no-cache bash ca-certificates &&\
apk add curl
# 和上个阶段一样设置工作目录
RUN mkdir -p /app/logs
WORKDIR /app
# 这一步不再从宿主机拷贝二进制文件,而是从上一个阶段构建的 builder 容器中拉取
COPY --from=builder /app/micro-user-service/micro-user-service .
# 启动用户服务
CMD ["./micro-user-service"]
为什么适用多阶段构建?
我们知道在docker镜像中每增加一个指令,镜像都会产生一个新的层,等层级越多,一个镜像就越臃肿,运行效率更低,占用资源更多。一个高效的dockerfile应该在实际运行中清除掉不需要的资源。像我们在程序执行中其实只依赖一个编译好的可执行文件,所以我们并不依赖go的环境,当我们在第一阶段将通过go镜像编译好之后,这些资源便可以抛弃掉,从而达到一个镜像瘦身的效果。
编译运行服务
上面我们已经书写好dockerfile,这是我们通过docker-compose来对dockerfile编译并且部署,使其注册到服务中心去。
修改docker-compose.yaml
...
micro-user-service:
depends_on: # 启动依赖,需要等etcd集群启动后才启动当前容器
- etcd1
- etcd2
- etcd3
build: ./user # dockerfile所在目录
environment:
MICRO_SERVER_ADDRESS: ":9091" # 服务端口
MICRO_REGISTRY: "etcd" # 注册中心类型
MICRO_REGISTRY_ADDRESS: "etcd1:2379,etcd2:2379,etcd3:2379" # 注册中心集群地址
ports:
- 9092:9091
networks:
- micro-network
...
执行docker-compose up -d micro-user-service
得益于go-micro良好的代码机制,我们无需修改任何代码就可以通过设置环境变量直接指定注册中心驱动以及地址。当服务运行时会默认读取环境变量在代码中执行,将服务注册到服务中。但是这些环境变量设置并无相关文档说明,需要阅读源码或者搜索得到零星的说明。文档缺失,是我使用go-micro开发时的痛苦根源。
需要注意的是,我们在编译镜像的时候经常会因为网络原因导致编译耗时非常之久,如果在本地开发的时候频繁修改代码后每次都需要编译执行会使我们的效率相当之低,这里我说下我的解决方法。
第一次编译通过之后,每次修改代码后不对镜像进行重新构建。我们直接编译项目的可执行文件,然后将整个代码目录其挂载在容器之中,然后重启容器。就可以马上看到代码修改的效果了,等到正式上线去掉挂载之后再重新构建镜像。
在docker-compose对应的服务中加上一行 volumes
...
micro-user-service:
depends_on: # 启动依赖,需要等etcd集群启动后才启动当前容器
- etcd1
- etcd2
- etcd3
build: ./user # dockerfile所在目录
environment:
MICRO_SERVER_ADDRESS: ":9091" # 服务端口
MICRO_REGISTRY: "etcd" # 注册中心类型
MICRO_REGISTRY_ADDRESS: "etcd1:2379,etcd2:2379,etcd3:2379" # 注册中心集群地址
ports:
- 9092:9091
volumes:
- ./user:/app
networks:
- micro-network
...
检查服务是否注册成功
打开dockerdesktop检查容器是否正常运行
打开micro-web的services
可以看到micro.service.user已经显示在列表,点击详情相关的rpc方法也已经存在,至此我们第一个服务已经注册成功。