内容简介:参考:从Kubernetes 1.11开始,可使用CoreDNS作为Kubernetes的DNS插件进入GA状态,Kubernetes推荐使用CoreDNS作为集群内的DNS服务。 我们先看一下Kubernetes DNS服务的发展历程。Kubernetes 1.3之前的版本使用skyDNS作为DNS服务,这个有点久远了。Kubernetes的DNS服务由kube2sky、skyDNS、etcd组成。 kube2sky通过kube-apiserver监听集群中Service的变化,将生成的DNS记录信息更新
参考:
- 官方网站, https://coredns.io/
- CoreDNS安装, https://my.oschina.net/u/2306127/blog/1618543
- CoreDNS使用手册, https://coredns.io/manual/toc/
- CoreDNS源码, https://github.com/coredns
- CoreDNS配置, https://my.oschina.net/u/2306127/blog/1788566
一.Kubernetes DNS服务发展史
从Kubernetes 1.11开始,可使用CoreDNS作为Kubernetes的DNS插件进入GA状态,Kubernetes推荐使用CoreDNS作为集群内的DNS服务。 我们先看一下Kubernetes DNS服务的发展历程。
1.1 Kubernetes 1.3之前的版本 – skyDNS
Kubernetes 1.3之前的版本使用skyDNS作为DNS服务,这个有点久远了。Kubernetes的DNS服务由kube2sky、skyDNS、etcd组成。 kube2sky通过kube-apiserver监听集群中Service的变化,将生成的DNS记录信息更新到etcd中,而skyDNS将从etcd中获取数据对外提供DNS的查询服务。
1.2 Kubernetes 1.3版本开始 – kubeDNS
Kubernetes 1.3开始使用kubeDNS和dnsmasq替换了原来的kube2sky和skyDNS,不再使用etcd,而是将DNS记录直接存放在内存中,通过dnsmasq的缓存功能提高DNS的查询效率。下图是描述了Kubernetes使用kubeDNS实现服务发现的整体架构:
1.3 Kubernetes 1.11版本开始 – CoreDNS进入GA
从Kubernetes 1.11开始,可使用CoreDNS作为Kubernetes的DNS插件进入GA状态,Kubernetes推荐使用CoreDNS作为集群内的DNS服务。 CoreDNS从2017年初就成为了CNCF的的孵化项目,CoreDNS的特点就是十分灵活和可扩展的插件机制,
各种插件实现:
不同的功能,如重定向、定制DNS记录、记录日志等等。下图描述了CoreDNS的整体架构:
二、CoreDNS简介
Kubernetes包括用于服务发现的DNS服务器Kube-DNS。 该DNS服务器利用SkyDNS的库来为Kubernetes pod和服务提供DNS请求。SkyDNS2的作者,Miek Gieben,创建了一个新的DNS服务器,CoreDNS,它采用更模块化,可扩展的框架构建。 Infoblox已经与Miek合作,将此DNS服务器作为Kube-DNS的替代品。
CoreDNS利用作为Web服务器Caddy的一部分而开发的服务器框架。该框架具有非常灵活,可扩展的模型,用于通过各种中间件组件传递请求。这些中间件组件根据请求提供不同的操作,例如记录,重定向,修改或维护 。虽然它一开始作为Web服务器,但是Caddy并不是专门针对HTTP协议的,而是构建了一个基于CoreDNS的理想框架。
在这种灵活的模型中添加对Kubernetes的支持,相当于创建了一个Kubernetes中间件。该中间件使用Kubernetes API来满足针对特定Kubernetes pod或服务的DNS请求。而且由于Kube-DNS作为Kubernetes的另一项服务,kubelet和Kube-DNS之间没有紧密的绑定。您只需要将DNS服务的IP地址和域名传递给kubelet,而Kubernetes并不关心谁在实际处理该IP请求。
1、CoreDNS支持行为
1.0.0版本主要遵循Kube-DNS的当前行为。 CoreDNS的005及更高版本实现了完整的规范和更多功能。
- A记录(正常的Service分配了一个名为my-svc.my-namespace.svc.cluster.local的DNS A记录。 这解决了服务的集群IP)
- “headless”(没有集群IP)的Service也分配了一个名为my-svc.my-namespace.svc.cluster.local的DNS A记录。 与普通服务不同,这解决了Service选择了pods的一组IP。 客户预计将从这ip集合中消耗集合或使用标准循环选择。
- 针对名为正常或无头服务的端口创建的SRV记录,对于每个命名的端口,SRV记录的格式为_my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local。对于常规服务,这将解析为端口号和CNAME:my-svc.my-namespace.svc.cluster.local;对于无头服务,这解决了多个答案,一个用于支持服务的每个pod,并包含端口号还有格式为auto-generated-name.my-svc.my-namespace.svc.cluster.local 的pod的CNAME 。SRV记录包含它们中的“svc”段,对于省略“svc”段的旧式CNAME不支持。
- 作为Service一部分的endpoints的A记录(比如“pets”的记录)
- pod的Spec中描述的A记录
- 还有就是用来发现正在使用的DNS模式版本的TXT记录
所有群集中不需要pod A记录支持,默认情况下禁用。 此外,CoreDNS对此用例的支持超出了在Kube-DNS中找到的标准行为。
在Kube-DNS中,这些记录不反映集群的状态,例如,对w-x-y-z.namespace.pod.cluster.local的任何查询将返回带有w.x.y.z(ip)的A记录,即使该IP不属于指定的命名空间,甚至不属于集群地址空间。最初的想法是启用对* .namespace.pod.cluster.local这样的域使用通配符SSL证书。
CoreDNS集成了提供pod验证的选项,验证返回的IP地址w.x.y.z实际上是指定命名空间中的pod的IP。他防止在命名空间中欺骗DNS名称。 然而,它确实会大大增加CoreDNS实例的内存占用,因为现在它需要观察所有的pod,而不仅仅是服务端点。
2、架构
整个 CoreDNS 服务都建立在一个使用 Go 编写的 HTTP/2 Web 服务器 Caddy · GitHub 上,CoreDNS 整个项目可以作为一个 Caddy 的教科书用法。
CoreDNS 的大多数功能都是由插件来实现的,插件和服务本身都使用了 Caddy 提供的一些功能,所以项目本身也不是特别的复杂。
3、插件
作为基于 Caddy 的 Web 服务器,CoreDNS 实现了一个插件链的架构,将很多 DNS 相关的逻辑都抽象成了一层一层的插件,包括 Kubernetes 等功能,每一个插件都是一个遵循如下协议的结构体:
type (
Plugin func(Handler) Handler
Handler interface {
ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error)
Name() string
}
)
所以只需要为插件实现 ServeDNS 以及 Name 这两个接口并且写一些用于配置的代码就可以将插件集成到 CoreDNS 中。
4、Corefile
另一个 CoreDNS 的特点就是它能够通过简单易懂的 DSL 定义 DNS 服务,在 Corefile 中就可以组合多个插件对外提供服务:
coredns.io:5300 {
file db.coredns.io
}
example.io:53 {
log
errors
file db.example.io
}
example.net:53 {
file db.example.net
}
.:53 {
kubernetes
proxy . 8.8.8.8
log
errors
cache
}
对于以上的配置文件,CoreDNS 会根据每一个代码块前面的区和端点对外暴露两个端点提供服务:
该配置文件对外暴露了两个 DNS 服务,其中一个监听在 5300 端口,另一个在 53 端口,请求这两个服务时会根据不同的域名选择不同区中的插件进行处理。
原理
CoreDNS 可以通过四种方式对外直接提供 DNS 服务,分别是 UDP、gRPC、HTTPS 和 TLS:
但是无论哪种类型的 DNS 服务,最终队会调用以下的 ServeDNS 方法,为服务的调用者提供 DNS 服务:
func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) {
m, _ := edns.Version(r)
ctx, _ := incrementDepthAndCheck(ctx)
b := r.Question[0].Name
var off int
var end bool
var dshandler *Config
w = request.NewScrubWriter(r, w)
for {
if h, ok := s.zones[string(b[:l])]; ok {
ctx = context.WithValue(ctx, plugin.ServerCtx{}, s.Addr)
if r.Question[0].Qtype != dns.TypeDS {
rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
dshandler = h
}
off, end = dns.NextLabel(q, off)
if end {
break
}
}
if r.Question[0].Qtype == dns.TypeDS && dshandler != nil && dshandler.pluginChain != nil {
rcode, _ := dshandler.pluginChain.ServeDNS(ctx, w, r)
plugin.ClientWrite(rcode)
return
}
if h, ok := s.zones["."]; ok && h.pluginChain != nil {
ctx = context.WithValue(ctx, plugin.ServerCtx{}, s.Addr)
rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
plugin.ClientWrite(rcode)
return
}
}
在上述这个已经被简化的复杂函数中,最重要的就是调用了『插件链』的 ServeDNS 方法,将来源的请求交给一系列插件进行处理,如果我们使用以下的文件作为 Corefile:
example.org {
file /usr/local/etc/coredns/example.org
prometheus # enable metrics
errors # show errors
log # enable query logs
}
那么在 CoreDNS 服务启动时,对于当前的 example.org 这个组,它会依次加载 file 、 log 、 errors 和 prometheus 几个插件,这里的顺序是由 zdirectives.go 文件定义的,启动的顺序是从下到上:
var Directives = []string{
// ...
"prometheus",
"errors",
"log",
// ...
"file",
// ...
"whoami",
"on",
}
因为启动的时候会按照从下到上的顺序依次『包装』每一个插件,所以在真正调用时就是从上到下执行的,这就是因为 NewServer 方法中对插件进行了组合:
func NewServer(addr string, group []*Config) (*Server, error) {
s := &Server{
Addr: addr,
zones: make(map[string]*Config),
connTimeout: 5 * time.Second,
}
for _, site := range group {
s.zones[site.Zone] = site
if site.registry != nil {
for name := range enableChaos {
if _, ok := site.registry[name]; ok {
s.classChaos = true
break
}
}
}
var stack plugin.Handler
for i := len(site.Plugin) - 1; i >= 0; i-- {
stack = site.Plugin[i](stack)
site.registerHandler(stack)
}
site.pluginChain = stack
}
return s, nil
}
对于 Corefile 里面的每一个配置组, NewServer 都会讲配置组中提及的插件按照一定的顺序组合起来,原理跟 Rack Middleware 的机制非常相似,插件 Plugin 其实就是一个出入参数都是 Handler 的函数:
type (
Plugin func(Handler) Handler
Handler interface {
ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error)
Name() string
}
)
所以我们可以将它们叠成堆栈的方式对它们进行操作,这样在最后就会形成一个插件的调用链,在每个插件执行方法时都可以通过 NextOrFailure 函数调用下一个插件的 ServerDNS 方法:
func NextOrFailure(name string, next Handler, ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
if next != nil {
if span := ot.SpanFromContext(ctx); span != nil {
child := span.Tracer().StartSpan(next.Name(), ot.ChildOf(span.Context()))
defer child.Finish()
ctx = ot.ContextWithSpan(ctx, child)
}
return next.ServeDNS(ctx, w, r)
}
return dns.RcodeServerFailure, Error(name, errors.New("no next plugin found"))
}
除了通过 ServeDNS 调用下一个插件之外,我们也可以调用 WriteMsg 方法并结束整个调用链。
从插件的堆叠到顺序调用以及错误处理,我们对 CoreDNS 的工作原理已经非常清楚了,接下来我们可以简单介绍几个插件的作用。
三、在kubernetes中部署coredns
1、下载coredns部署包并说明
https://github.com/coredns/deployment/tree/master/kubernetes
主要有几个文件:
deploy.sh是一个便捷的脚本,用于生成用于在当前运行标准kube-dns的集群上运行CoreDNS的清单。使用coredns.yaml.sed文件作为模板,它创建一个ConfigMap和一个CoreDNS deployment,然后更新 Kube-DNS service selector以使用CoreDNS deployment。 通过重新使用现有服务,服务请求不会中断。
脚本不会删除kube-dns的deployment或replication controller - 您必须手动执行:
kubectl delete --namespace=kube-system deployment kube-dns
要使用它,只需将它们放在同一目录中,然后运行deploy.sh脚本 ,将其传递给您的服务CIDR(10.3.0.0/24) 。 这将生成具有必要Corefile的ConfigMap。 它还将查找现有的kube-dns服务的集群IP。
[root@k8s-master conf.d]# etcdctl ls /k8s/network/subnets /k8s/network/subnets/10.0.24.0-24 /k8s/network/subnets/10.0.86.0-24 /k8s/network/subnets/10.0.35.0-24
(注意:以上原始脚本只适用于当前kubernetes集群含有kube-dns的情况,如果没有需要修改下脚本
#!/bin/bash 10.0.24.200
# Deploys CoreDNS to a cluster currently running Kube-DNS.
SERVICE_CIDR=$1
CLUSTER_DOMAIN=${2:-cluster.local}
YAML_TEMPLATE=${3:-`pwd`/coredns.yaml.sed}
YAML=${4:-`pwd`/coredns.yaml}
if [[ -z $SERVICE_CIDR ]]; then
echo "Usage: $0 SERVICE-CIDR [ CLUSTER-DOMAIN ] [ YAML-TEMPLATE ] [ YAML ]"
exit 1
fi
#CLUSTER_DNS_IP=$(kubectl get service --namespace kube-system kube-dns -o jsonpath="{.spec.clusterIP}")
CLUSTER_DNS_IP=
默认情况下CLUSTER_DNS_IP是自动获取kube-dns的集群ip的,但是由于没有部署kube-dns所以只能手动指定一个集群ip了。
执行: ./deploy.sh 10.0.0.0/24 cluster.local
以上脚本执行后可以看到预览的效果。
仔细观察上面的Corefile部分,这是一个在端口53上运行CoreDNS并为Kubernetes提供cluster.local域的示例
.:53 {
errors
log stdout
health
kubernetes cluster.local 10.3.0.0/24
proxy . /etc/resolv.conf
cache 30
}
1)errors官方没有明确解释,后面研究
2)log stdout:日志中间件配置为将日志写入STDOUT
3)health:健康检查,提供了指定端口(默认为8080)上的HTTP端点,如果实例是健康的,则返回“OK”。
4)cluster.local:CoreDNS为kubernetes提供的域,10.3.0.0/24这告诉Kubernetes中间件它负责为反向区域提供PTR请求0.0.3.10.in-addr.arpa ..换句话说,这是允许反向DNS解析服务(我们经常使用到得DNS服务器里面有两个区域,即“正向查找区域”和“反向查找区域”,正向查找区域就是我们通常所说的域名解析,反向查找区域即是这里所说的IP反向解析,它的作用就是通过查询IP地址的PTR记录来得到该IP地址指向的域名,当然,要成功得到域名就必需要有该IP地址的PTR记录。PTR记录是邮件交换记录的一种,邮件交换记录中有A记录和PTR记录,A记录解析名字到地址,而PTR记录解析地址到名字。地址是指一个客户端的IP地址,名字是指一个客户的完全合格域名。通过对PTR记录的查询,达到反查的目的。)
5)proxy:这可以配置多个upstream 域名服务器,也可以用于延迟查找 /etc/resolv.conf 中定义的域名服务器
6)cache:这允许缓存两个响应结果,一个是肯定结果(即,查询返回一个结果)和否定结果(查询返回“没有这样的域”),具有单独的高速缓存大小和TTLs。
2、现在安装coredns到kubernetes中
# ./deploy.sh -r 10.0.0.0/16 -i 192.168.10.90 -d cluster.local -t coredns.yaml.sed -s > coredns.yaml
# .kubectl apply -f coredns.yaml
或者直接执行:
# ./deploy.sh -r 10.0.0.0/16 -i 192.168.10.90 -d cluster.local -t coredns.yaml.sed -s | kubectl apply -f -
查看service
[root@k8s-master coredns]# kubectl get service -l k8s-app=kube-dns --namespace=kube-system NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kube-dns ClusterIP 192.168.10.90 <none> 53/UDP,53/TCP,9153/TCP 15d
查看coredns的Pod,确认所有Pod都处于Running状态:
kubectl get pods -n kube-system -l k8s-app=kube-dns
查看日志:# kubectl logs -f coredns-6cc7bf59f4-4rfq8 --namespace=kube-system
3、修改cluster-dns
修改master节点和所有node节点--cluster-dns配置,修改内容如红色所注,与上面的Corefile中的值对应。
4、测试CoreDNS
现在我们来创建一个wepapp的pod和service,测试一下coredns是否起作用
[root@k8s-master conf.d]# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
webapp-nrz4t 1/1 Running 0 1d 10.0.35.3 192.168.10.39
webapp-zp69q 1/1 Running 0 1d 10.0.24.4 192.168.10.50
[root@k8s-master ~]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 192.168.0.1 <none> 443/TCP 28d
webapp ClusterIP 192.168.14.242 <none> 9081/TCP 17d
webapp2 ClusterIP 192.168.22.2 <none> 9082/TCP 17d
[root@k8s-master ~]# curl 192.168.22.2:9082
Hello world
1、检查集群的pod /etc/resolv.conf是否生效:
首先进入这个集群内的另一个pod
kubectl exec -it webapp-nrz4t /bin/sh 或者 docker exec -it 0d0874df9e15 /bin/sh
$ cat /etc/resolv.conf
sh-4.2# cat /etc/resolv.conf
nameserver 192.168.10.90
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
2、curl测试服务:
curl webapp.default.svc.cluster.local:9081
3、在master的主机上修改 /etc/resolv.conf,增加一行:nameserver 192.168.10.90后执行
curl webapp.default.svc.cluster.local:9081
通过域名访问成功!
问题排查技巧
如果执行 nslookup 命令失败,检查如下内容:
1、先检查本地 DNS 配置
查看配置文件 resolv.conf。
按照如下方法(注意搜索路径可能会因为云提供商不同而变化)验证搜索路径和 Name Server 的建立:
nameserver 192.168.10.90
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
2、快速诊断
出现类似如下指示的错误,说明 kube-dns 插件或相关 Service 存在问题:
检查service是否正常运行:
kubectl get svc --namespace=kube-system
或者检查是否 DNS Pod 正在运行
使用 kubectl get pods 命令验证 DNS Pod 正在运行:
kubectl get pods --namespace=kube-system -l k8s-app=kube-dns
应该能够看到类似如下信息:
如果看到没有 Pod 运行,或 Pod 失败/结束,DNS 插件不能默认部署到当前的环境,必须手动部署。
或者查看service的Endpoint是否正常:
kubectl get ep kube-dns --namespace=kube-system
如果没有看到 Endpoint,查看 调试 Service 文档 中的 Endpoint 段内容。
3、检查 DNS Pod 中的错误信息
使用 kubectl logs 命令查看 DNS 后台进程的日志:
kubectl logs coredns-6cc7bf59f4-vj7cc -n kube-system
看到错误信息:
eflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Namespace: Get https://192.168.0.1:443/api/v1/namespace
2.168.0.1:443/api/v1/endpoints?limit=500&resourceVersion=0: x509: certificate is valid for 192.168.10.50, 10.0.0.1, not 192.168.0.1
E0710 16:00:58.986103 1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Namespace: Get https://192.168.0.1:443/api/v1/namespaces?limit=500&resourceVersion=0: x509: certificate is valid for 192.168.10.50, 10.0.0.1, not 192.168.0.1
E0710 16:00:58.989633 1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Service: Get https://192.168.0.1:443/api/v1/services?limit=500&resourceVersion=0: x509: certificate is valid for 192.168.10.50, 10.0.0.1, not 192.168.0.1
E0710 16:00:58.991655 1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Endpoints: Get https://192.168.0.1:443/api/v1/endpoints?limit=500&resourceVersion=0: x509: certificate is valid for 192.168.10.50, 10.0.0.1, not 192.168.0.1
E0710 16:00:59.990732 1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+inco
原因:192.168.0.1不在证书里面
解决:需要重新设置:apiserver证书
masterssl.cnf文件的示例如下:IP.3 = 192.168.0.1
[req] req_extensions = v3_req distinguished_name = req_distinguished_name [req_distinguished_name] [ v3_req ] basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment subjectAltName = @alt_names [alt_names] DNS.1 = kubernetes DNS.2 = kubernetes.default DNS.3 = kubernetes.default.svc DNS.4 = kubernetes.default.svc.cluster.local IP.1 = ${K8S_SERVICE_IP} IP.2 = ${MASTER_IPV4}
IP.3 = 192.168.0.1
错误信息:
E0712 02:25:45.326123 1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Service: Unauthorized
E0712 02:25:45.327038 1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Endpoints: Unauthorized
E0712 02:25:45.328029 1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Namespace: Unauthorized
E0712 02:25:46.327153 1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Service: Unauthorized
E0712 02:25:46.328210 1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Endpoints: Unauthorized
E0712 02:25:46.329160 1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Namespace: Unauthorized
E0712 02:25:47.328243 1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Service: Unauthorized
E0712 02:25:47.329143 1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Endpoints: Unauthorized
E0712 02:25:47.330086 1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Namespace: Unauthorized
原因:service account 的token认知不通过,是因为重新生成证书后,需要删除旧的token。
解决:
kubectl get secret -n kube-system
NAME TYPE DATA AGE
coredns-token-cdn9x kubernetes.io/service-account-token 3 3d
default-token-lht2v kubernetes.io/service-account-token 3 3d
kubectl delete secret coredns-token-cdn9x -n kube-system
secret "coredns-token-cdn9x" deleted
以上所述就是小编给大家介绍的《k8s实践(11) --服务发现CoreDNS详解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 非面试向跨域实践详解
- 从一个实例详解敏捷测试的最佳实践
- Nginx 从入门到实践,万字详解
- 爱奇艺 iOS 深度实践:SiriKit 详解应用篇
- 五大实例详解,携程 Redis 跨机房双向同步实践
- 五大实例详解,携程 Redis 跨机房双向同步实践
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Beginning Google Maps API 3
Gabriel Svennerberg / Apress / 2010-07-27 / $39.99
This book is about the next generation of the Google Maps API. It will provide the reader with the skills and knowledge necessary to incorporate Google Maps v3 on web pages in both desktop and mobile ......一起来看看 《Beginning Google Maps API 3》 这本书的介绍吧!