最近一直被这个连接问题给困恼。。。怎么都没法连接上阿里云的云服务器。对于 golang 我用的是 github.com/mongodb/mongo-go-driver
,每一项都按照了文档去设置但是死活连接不上,于是更换为 github.com/globalsign/mgo
,这样就可以正常连接。对于 python,如果使用了 pymongo4.0.1
版本也是无法连接,但是更换为 pymongo3.6
就可以正常连接。
这两个现象都非常奇怪,但都有相同的特点,无论是 golang
里的 mgo
库还是 python
的 pymong3.6
都是挺老的版本。
# golang 报错日志:
2022/01/23 21:38:18 server selection error: server selection timeout, current topology: { Type: ReplicaSetNoPrimary, Servers: [{ Addr: 139.196.245.210:3717, Type: Unknown, Last error: connection() error occured during connection handshake: connection(139.196.245.210:3717[-127]) socket was unexpectedly closed: EOF }, { Addr: 139.196.245.214:3717, Type: Unknown, Last error: connection() error occured during connection handshake: connection(139.196.245.214:3717[-128]) socket was unexpectedly closed: EOF }, ] }
# python 报错日志
pymongo.errors.ServerSelectionTimeoutError: 139.196.245.214:3717: connection closed,139.196.245.210:3717: connection closed, Timeout: 30s, Topology Description: <TopologyDescription id: 61ed5945eba641d6e1b58800, topology_type: ReplicaSetNoPrimary, servers: [<ServerDescription ('139.196.245.210', 3717) server_type: Unknown, rtt: None, error=AutoReconnect('139.196.245.210:3717: connection closed')>, <ServerDescription ('139.196.245.214', 3717) server_type: Unknown, rtt: None, error=AutoReconnect('139.196.245.214:3717: connection closed')>]>
看到这个错误日志真的令人迷惑,为啥会出现这么多的 server,我只是通过跳板机访问了一个服务。
查找资料后发现,这些云数据库都是容器化管理,也就是我的一台 mongodb 云数据库,其实有多个容器组成的集群,这些容器之间可以相互访问,但是外部无法访问集群的节点。
这里非常感谢,连接 Replica Set 出现问题给出了解释:
MongoDB driver will attempt
server discovery
from given a replica set member(s); it will find all of other nodes within the replica set (via rs.conf). The problem here is the replica set is set with namemongo<N>
, the driver (run in Docker host) would not be able to resolve these names. You can confirm this by trying to pingmongo1
from Docker host.You can either try running the application from another Docker instance sharing the same Docker network as the replica set. Or, modify the Docker networking as such to allow resolvable hostnames.
UPDATE:
Regarding your comment on why using mongo shell, or PyMongo works.
This is due to the difference in connection mode. When specifying a single node, i.e.
mongodb://node1:27017
in shell or PyMongo, server discovery are not being made. Instead it will attempt to connect to that single node (not as part as a replica set). The catch is that you need to connect to the primary node of the replica set to write (you have to know which one). If you would like to connect as a replica set, you have to define the replica set name.In contrast to the
mongo-go-driver
, by default it would perform server discovery and attempt to connect as a replica set. If you would like to connect as a single node, then you need to specifyconnect=direct
in the connection URI.
也就是说 driver 会默认开启 服务发现
,这就导致我们会从容器的外部来访问这个集群的其他机器。
在 pymongo
和 mgo
这些比较旧的服务器里,因为那时还没有流行这种集群化管理,所以没有服务发现的功能。
# 解决办法
In contrast to the
mongo-go-driver
, by default it would perform server discovery and attempt to connect as a replica set. If you would like to connect as a single node, then you need to specifyconnect=direct
in the connection URI.
采用 direct
的连接方式。
这里可以看 golang
给出的 docs
:
package main | |
import ( | |
"context" | |
"log" | |
"go.mongodb.org/mongo-driver/mongo" | |
"go.mongodb.org/mongo-driver/mongo/options" | |
) | |
func main() { | |
// Create a direct connection to a host. The driver will send all requests | |
// to that host and will not automatically discover other hosts in the | |
// deployment. | |
clientOpts := options.Client().ApplyURI( | |
"mongodb://localhost:27017/?connect=direct") | |
client, err := mongo.Connect(context.TODO(), clientOpts) | |
if err != nil { | |
log.Fatal(err) | |
} | |
_ = client | |
} |
ApplyURI
把 connect=direct
加入,这样就可以愉快连接了。
# pymongo
client = MongoClient('mongodb://localhost', | |
port=3733, | |
username=username, | |
password=password, | |
authSource='admin', | |
directConnection =True, | |
authMechanism='SCRAM-SHA-1' | |
) |
pymongo
中有个字段 directConnection
,这个字段设置为 True
代表直接连接。
# 这里附上我连接的代码
# golang
package main | |
import ( | |
"context" | |
"log" | |
"os" | |
"time" | |
"github.com/elliotchance/sshtunnel" | |
"go.mongodb.org/mongo-driver/mongo" | |
"go.mongodb.org/mongo-driver/mongo/options" | |
"golang.org/x/crypto/ssh" | |
) | |
func main() { | |
tunnel := sshtunnel.NewSSHTunnel( | |
// 在这里设置你的跳板机地址. | |
"username@ipv4:port", | |
// 选择 sshpassword 的连接方式 | |
ssh.Password("password"), | |
// 阿里云 mongodb 的地址. | |
"dds-uf61fd4**********44-pub.mongodb.rds.aliyuncs.com:3717", | |
// 设置本地绑定端口 | |
"3733", | |
) | |
tunnel.Log = log.New(os.Stdout, "", log.Ldate|log.Lmicroseconds) | |
go tunnel.Start() | |
time.Sleep(100 * time.Millisecond) // 等待开启 tunnel | |
MgoCli() | |
} | |
var mgoCli *mongo.Client | |
func initDb() { | |
var err error | |
credential := options.Credential{ | |
AuthMechanism: "SCRAM-SHA-1", // 阿里云服务的 | |
Username: "username", //mongodb 用户名 | |
Password: "password", //mongodb 密码 | |
AuthSource: "admin", // 默认 admin 不需要改 | |
PasswordSet: true, | |
} | |
clientOpts := options.Client().ApplyURI("mongodb://localhost:3733"). | |
SetAuth(credential) | |
// 连接到 MongoDB | |
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // 设置 5s 超时 | |
defer cancel() | |
client, err := mongo.Connect(ctx, clientOpts) | |
if err != nil { | |
log.Fatal(err) | |
} | |
// 检查连接 | |
err = client.Ping(context.TODO(), nil) | |
if err != nil { | |
log.Fatal(err) | |
} | |
} | |
func MgoCli() *mongo.Client { | |
if mgoCli == nil { | |
initDb() | |
} | |
return mgoCli | |
} |
# pymongo
from pymongo import MongoClient | |
from sshtunnel import SSHTunnelForwarder | |
from pprint import pprint | |
import urllib.parse | |
import time | |
tunnel = SSHTunnelForwarder( | |
("跳板机ip",22), | |
ssh_username=r"跳板机用户名", | |
ssh_password=r"跳板机密码", | |
remote_bind_address=(r"dds-uf61f**********b.mongodb.rds.aliyuncs.com", 3717), | |
local_bind_address=("127.0.0.1",3733)) | |
tunnel.start() | |
print(tunnel.local_bind_port) | |
from pymongo import MongoClient | |
client = MongoClient('mongodb://localhost', | |
port=3733, | |
username='数据库用户名', | |
password='数据库密码', | |
authSource='admin', | |
directConnection =True, # 使用直接连接方式 | |
authMechanism='SCRAM-SHA-256' | |
) | |
db = client['admin'] | |
npm = db['npm_records'] | |
for item in npm.find().limit(1): | |
pprint(item) | |
tunnel.close() |