源码:https://github.com/google/trillian
编译及安装
go get github.com/google/trillian
cd $GOPATH/src/github.com/google/trillian
go get -t -u -v ./...
可执行程序
主要由三个进程 trillian_log_server, trillian_log_signer 和 trillian_map_server
- trillian_log_server
./trillian_log_server ${ETCD_OPTS} ${pkcs11_opts} ${logserver_opts} --rpc_endpoint="localhost:${port}" --http_endpoint="localhost:${http}" &
- trillian_log_signer
./trillian_log_signer ${ETCD_OPTS} ${pkcs11_opts} ${logsigner_opts} --sequencer_interval="1s" --batch_size=500 --http_endpoint="localhost:${http}" --num_sequencers 2 &
3 trillian_map_server
注意,上述步骤启动的两个服务,分别提供 rpc 服务和 http 服务,rpc 服务(admin_server 和 log_server)使用的端口默认是 8090,http 服务使用的端口默认是 8091启动服务时,会连接默认的 MySQL 数据库 test:zaphod@tcp(127.0.0.1:3306)/test ,所以,本机测试的时候,需要事先确保有相应的连接权限。
先启动一个本地的 etcd 服务器(执行 etcd
或者 etcd -data-dir=/var/edata
)
etcd 服务器的主要目的是注册当前的 grpc 二进制服务器,每个服务器对应一个 etcd 存储中的 key。
启动 trillian_log_server 之后,我们编译 cmd/createtree/main.go
目录,生成一个客户端进程 createtree,这个客户端进程通过调用 trillian_log_server 提供的 grpc 服务来创建一个 tree,并返回一个 treeId。
./createtree --admin_server="localhost:8090" \
--pem_key_path=../../testdata/log-rpc-server.privkey.pem \
--pem_key_password=towel \
--signature_algorithm=ECDSA
创建 Tree 的过程
在 cmd/createtree/main.go
中会构建一个 trillian.CreateTreeRequest,
构建好 request 对象之后,调用 trillian.NewTrillianAdminClient(conn).CreateTree(ctx, req) 来创建一个 Tree,并返回一个 TreeID。
代码组织方式
这里调用的服务是 TrillianAdminServer 提供的 CreateTree 服务(在 server/admin/admin_server.go 中),但是这个 TrillianAdminServer 却不是通过一个独立的服务进程(main.go)来提供的,而是被封装在 server/main.go 中的 Main 结构体中,然后通过 server/trillian_log_server/main.go 来使用 Main 注册 grpc 服务而提供的。
因为 grpc 根据 trillian_admin_api.proto 生成的 trillian_admin_api.pb.go 代码中 是通过一个 TrillianAdminServer interface 类型来提供服务的
type TrillianAdminServer interface {
// Lists all trees the requester has access to.
ListTrees(context.Context, *ListTreesRequest) (*ListTreesResponse, error)
// Retrieves a tree by ID.
GetTree(context.Context, *GetTreeRequest) (*Tree, error)
// Creates a new tree.
// System-generated fields are not required and will be ignored if present,
// e.g.: tree_id, create_time and update_time.
// Returns the created tree, with all system-generated fields assigned.
CreateTree(context.Context, *CreateTreeRequest) (*Tree, error)
// Updates a tree.
// See Tree for details. Readonly fields cannot be updated.
UpdateTree(context.Context, *UpdateTreeRequest) (*Tree, error)
// Soft-deletes a tree.
// A soft-deleted tree may be undeleted for a certain period, after which
// it'll be permanently deleted.
// TODO(codingllama): Provide an undelete RPC.
DeleteTree(context.Context, *DeleteTreeRequest) (*google_protobuf5.Empty, error)
}
server/admin/admin_server.go 里定义了一个 Server 的 struct 来实现上述接口
// Server is an implementation of trillian.TrillianAdminServer.
type Server struct {
registry extension.Registry
}
// New returns a trillian.TrillianAdminServer implementation.
func New(registry extension.Registry) *Server {
return &Server{registry}
}
可见,Server 结构体里面通过 registry 来实现了扩展。
Trillian 提供的是 trillian_log_server 和 trillian_map_server 两个 grpc 服务端进程,具体的服务是通过 server/log_rpc_server.go
和 server/map_rpc_server.go
来实现的。
比如 QueueLeaf(),GetInclusionProof() 等。
Trillian 还用了一个 server/main.go
来封装把上述两个 server 封装在一个 Main 的 struct 中,如下:
// Main encapsulates the data and logic to start a Trillian server (Log or Map).
type Main struct {
// Endpoints for RPC and HTTP/REST servers.
// HTTP/REST is optional, if empty it'll not be bound.
RPCEndpoint, HTTPEndpoint string
DB *sql.DB
Registry extension.Registry
Server *grpc.Server
// RegisterHandlerFn is called to register REST-proxy handlers.
RegisterHandlerFn func(context.Context, *runtime.ServeMux, string, []grpc.DialOption) error
// RegisterServerFn is called to register RPC servers.
RegisterServerFn func(*grpc.Server, extension.Registry) error
}
// Run starts the configured server. Blocks until the server exits.
func (m *Main) Run(ctx context.Context) error {
//...
}
Main struct 中的 RPCEndpoint 和 HTTPEndpoint,服务端启动时,默认会使用 localhost:8090 作为 RPC 端口,localhost:8091 最为 http 端口。
var (
rpcEndpoint = flag.String("rpc_endpoint", "localhost:8090", "Endpoint for RPC requests (host:port)")
httpEndpoint = flag.String("http_endpoint", "localhost:8091", "Endpoint for HTTP metrics and REST requests on (host:port, empty means disabled)")
//...
)
这个 Main struct 实现了一个 Run,然后在 server/trillian_log_server/main.go
和 server/trillian_map_server/main.go
中就可以都通过调用 Main.Run() 来启动 grpc 服务。
这个地方的设计非常巧妙,两个进程都通过调用一个统一的封装好的结构体 Main 的 Run 函数来启动 grpc 服务。 然后。在这个统一的 Run 函数里面,我们又可以封装其他许多公共的服务,比如 http 服务,TrillianAdminServer 等。
这里本质上利用了一个 grpc server 可以注册多个实现,比如:
server := grpc.NewServer()
pb.RegisterSayHelloServer(server, NewHelloServer())
pb.RegisterSayGoodByeServer(server, NewGoodByeServer())
``
可见,上述代码为一个 grpc 服务器注册了多个实现。
#### Trillian 集成测试 integration test
执行下面两个脚本来进行 trillian_log_server 和 trillian_map_server 的集成测试:
integration/log_integration_test.sh
integration/map_integration_test.sh
集成测试时,需要准备环境,比如 integration/functions.sh 中的 log_prep_test() {} 函数,如下
```go
log_prep_test() {
go build ${GOFLAGS} github.com/google/trillian/server/trillian_log_server/
go build ${GOFLAGS} github.com/google/trillian/server/trillian_log_signer/
"${TRILLIAN_PATH}/scripts/resetdb.sh"
if [[ -x "${ETCD_DIR}/etcd" ]]; then
${ETCD_DIR}/etcd &
else
...
fi
./trillian_log_server ${ETCD_OPTS} ${pkcs11_opts} ${logserver_opts} --rpc_endpoint="localhost:${port}" --http_endpoint="localhost:${http}" &
./trillian_log_signer ${ETCD_OPTS} ${pkcs11_opts} ${logsigner_opts} --sequencer_interval="1s" --batch_size=500 --http_endpoint="localhost:${http}" --num_sequencers 2 &
}
会启动一定数量的 trillian_log_server 和 trillian_log_signer,默认是各一个。
同时,如果本地有 etcd 实例,那么会启动 etcd server。然后在启动 trillian_log_server 的时候通过 logserver_opts 的方式传入。
ETCD_OPTS="--etcd_servers=${etcd_server}"
logserver_opts="--etcd_http_service=trillian-logserver-http --etcd_service=trillian-logserver"
logsigner_opts="--etcd_http_service=trillian-logsigner-http"
pkcs11_opts="--pkcs11_module_path ${PKCS11_MODULE:-/usr/lib/softhsm/libsofthsm.so}"
# 启动 trillian_log_server
./trillian_log_server ${ETCD_OPTS} ${pkcs11_opts} ${logserver_opts} --rpc_endpoint="localhost:${rpc_port}" --http_endpoint="localhost:${http_port}" &
# 启动 trillian_log_signer
./trillian_log_signer ${ETCD_OPTS} ${pkcs11_opts} ${logsigner_opts} --sequencer_interval="1s" --batch_size=500 --http_endpoint="localhost:${http}" --num_sequencers 2 &
etcd 实际上是一个 k/v 数据库,因此,如果上述指定了 ETCD_OPTS,成功启动两个 trillian_log_server 和 trillian_log_signer 之后,它们会往 etcd 中写入值,这时候我们就应该能读取到写入的值:
if [[ ! -z "${ETCD_OPTS}" ]]; then
RPC_SERVERS="trillian-logserver"
echo "Registered log servers @${RPC_SERVERS}/"
ETCDCTL_API=3 etcdctl get ${RPC_SERVERS}/ --prefix
echo "Registered HTTP endpoints"
ETCDCTL_API=3 etcdctl get trillian-logserver-http/ --prefix
ETCDCTL_API=3 etcdctl get trillian-logsigner-http/ --prefix
fi
log_integration_test.sh 整个过程是这样的
KEY_ARGS="--pem_key_path=testdata/log-rpc-server.privkey.pem --pem_key_password=towel --signature_algorithm=ECDSA"
# 1. 启动 trillian_log_server
log_prep_test 1 1
# 2. 编译并执行客户端,调用 grpc 服务来创建一颗 Tree 并拿到返回的 TreeID
go build ${GOFLAGS} github.com/google/trillian/cmd/createtree/
TEST_TREE_ID=$(./createtree \
--admin_server="${RPC_SERVER_1}" \
${KEY_ARGS})
# 3. 根据拿到的 TreeId 来运行 log_integration_test.go 中的 test cases
go test -run ".*LiveLog.*" --timeout=5m ./ --treeid ${TEST_TREE_ID} --log_rpc_server="${RPC_SERVER_1}"
# 4. 停止 trillian_log_server
log_stop_test