最近一直被这个连接问题给困恼。。。怎么都没法连接上阿里云的云服务器。对于 golang 我用的是 github.com/mongodb/mongo-go-driver ,每一项都按照了文档去设置但是死活连接不上,于是更换为 github.com/globalsign/mgo ,这样就可以正常连接。对于 python,如果使用了 pymongo4.0.1 版本也是无法连接,但是更换为 pymongo3.6 就可以正常连接。

这两个现象都非常奇怪,但都有相同的特点,无论是 golang 里的 mgo 库还是 pythonpymong3.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 name mongo<N> , the driver (run in Docker host) would not be able to resolve these names. You can confirm this by trying to ping mongo1 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 specify connect=direct in the connection URI.

也就是说 driver 会默认开启 服务发现 ,这就导致我们会从容器的外部来访问这个集群的其他机器。

pymongomgo 这些比较旧的服务器里,因为那时还没有流行这种集群化管理,所以没有服务发现的功能。

# 解决办法

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 specify connect=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
}

ApplyURIconnect=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()