mirror of
https://github.com/milvus-io/milvus.git
synced 2026-07-21 18:25:44 +00:00
## What this PR does Makes the etcd client dial timeout configurable via `etcd.dialTimeout` (milliseconds, default `5000` — unchanged behavior). ## Why The dial timeout was hardcoded to 5s in `GetRemoteEtcdClient` / `GetRemoteEtcdClientWithAuth` / `GetRemoteEtcdSSLClientWithCfg`. On newly scaled-out nodes, transient network conditions during pod startup (sidecar proxy not fully ready, DNS propagation delay, or etcd connection pressure from many nodes starting at once) can push the blocking dial past 5s, causing the second etcd client created during `streaming.Init()` to panic with `context deadline exceeded`. Operators had no way to tune it. ## Changes - `pkg/util/etcd/etcd_util.go`: add `WithDialTimeout` client option (a non-positive value keeps the default). - `pkg/util/paramtable/service_param.go`: add `etcd.dialTimeout` param and wire it through `EtcdConfig.ClientOptions()`. - `configs/milvus.yaml`: document the new option. - `pkg/config/etcd_source.go` + `pkg/config/source.go` + `pkg/util/paramtable/base_table.go`: thread `dialTimeout` into the bootstrap config-source client as well (see below). Every paramtable-based etcd client honors it: meta kv, coordinator/node services, and the woodpecker WAL builder (the `streaming.Init()` path from the issue, which already passes `ClientOptions()...`). ## Bootstrap config-source client The bootstrap config-source client (`pkg/config/etcd_source.go`) is created before paramtable is fully ready, so it cannot read `etcd.dialTimeout` from etcd itself. But it does not need to: `initConfigsFromLocal()` loads the file/env config source *before* `initConfigsFromRemote()` builds the `EtcdInfo`, and the client's endpoints/auth/TLS are already read from there. `dialTimeout` is threaded through the same `EtcdInfo` path, so a value set in `milvus.yaml`/env applies to this client too. A zero/non-positive value is a no-op, so unconfigured behavior stays at the 5s default. The only remaining paths still on the fixed default are CLI/offline tools and the etcd `HealthCheck` (which uses a short fixed 3s context) — intended. issue: #50867 --------- Signed-off-by: xiaofanluan <xf@hjjaq.com> Signed-off-by: xiaofanluan <xiaofan.luan@zilliz.com> Co-authored-by: xiaofanluan <xf@hjjaq.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
362 lines
10 KiB
Go
362 lines
10 KiB
Go
// Licensed to the LF AI & Data foundation under one
|
|
// or more contributor license agreements. See the NOTICE file
|
|
// distributed with this work for additional information
|
|
// regarding copyright ownership. The ASF licenses this file
|
|
// to you under the Apache License, Version 2.0 (the
|
|
// "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package paramtable
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/milvus-io/milvus/pkg/v3/config"
|
|
"github.com/milvus-io/milvus/pkg/v3/mlog"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/etcd"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
|
|
)
|
|
|
|
// UniqueID is type alias of typeutil.UniqueID
|
|
type UniqueID = typeutil.UniqueID
|
|
|
|
const (
|
|
DefaultGlogConf = "glog.conf"
|
|
DefaultMinioHost = "localhost"
|
|
DefaultMinioPort = "9000"
|
|
DefaultMinioAccessKey = "minioadmin"
|
|
DefaultMinioSecretAccessKey = "minioadmin"
|
|
DefaultMinioUseSSL = "false"
|
|
DefaultMinioBucketName = "a-bucket"
|
|
DefaultMinioUseIAM = "false"
|
|
DefaultMinioCloudProvider = "aws"
|
|
DefaultMinioIAMEndpoint = ""
|
|
DefaultEtcdEndpoints = "localhost:2379"
|
|
|
|
DefaultLogFormat = "text"
|
|
DefaultLogLevelForBase = "debug"
|
|
DefaultRootPath = ""
|
|
DefaultMinioLogLevel = "fatal"
|
|
DefaultKnowhereThreadPoolNumRatioInBuild = 1
|
|
DefaultKnowhereThreadPoolNumRatioInBuildOfStandalone = 0.75
|
|
DefaultMinioRegion = ""
|
|
DefaultMinioUseVirtualHost = "false"
|
|
DefaultMinioRequestTimeout = "10000"
|
|
DefaultMinioMaxConnections = "100"
|
|
)
|
|
|
|
// Const of Global Config List
|
|
func globalConfigPrefixs() []string {
|
|
return []string{"metastore", "localStorage", "etcd", "tikv", "minio", "pulsar", "kafka", "rocksmq", "log", "grpc", "common", "quotaAndLimits", "trace"}
|
|
}
|
|
|
|
// support read "milvus.yaml", "_test.yaml", "default.yaml", "user.yaml" as this order.
|
|
// order: milvus.yaml < _test.yaml < default.yaml < user.yaml, do not change the order below.
|
|
// Use _test.yaml only for test related purpose.
|
|
var defaultYaml = []string{"milvus.yaml", "_test.yaml", "default.yaml", "user.yaml"}
|
|
|
|
// BaseTable the basics of paramtable
|
|
type BaseTable struct {
|
|
once sync.Once
|
|
mgr *config.Manager
|
|
config *baseTableConfig
|
|
}
|
|
|
|
type baseTableConfig struct {
|
|
configDir string
|
|
refreshInterval time.Duration
|
|
skipRemote bool
|
|
skipEnv bool
|
|
yamlFiles []string
|
|
}
|
|
|
|
type Option func(*baseTableConfig)
|
|
|
|
func Files(files []string) Option {
|
|
return func(bt *baseTableConfig) {
|
|
bt.yamlFiles = files
|
|
}
|
|
}
|
|
|
|
func Interval(interval time.Duration) Option {
|
|
return func(bt *baseTableConfig) {
|
|
bt.refreshInterval = interval
|
|
}
|
|
}
|
|
|
|
func SkipRemote(skip bool) Option {
|
|
return func(bt *baseTableConfig) {
|
|
bt.skipRemote = skip
|
|
}
|
|
}
|
|
|
|
func SkipEnv(skip bool) Option {
|
|
return func(bt *baseTableConfig) {
|
|
bt.skipEnv = skip
|
|
}
|
|
}
|
|
|
|
// NewBaseTableFromYamlOnly only used in migration tool.
|
|
// Maybe we shouldn't limit the configDir internally.
|
|
func NewBaseTableFromYamlOnly(yaml string) *BaseTable {
|
|
return NewBaseTable(Files([]string{yaml}), SkipRemote(true), SkipEnv(true))
|
|
}
|
|
|
|
func NewBaseTable(opts ...Option) *BaseTable {
|
|
defaultConfig := &baseTableConfig{
|
|
configDir: initConfPath(),
|
|
yamlFiles: defaultYaml,
|
|
refreshInterval: 5 * time.Second,
|
|
skipRemote: false,
|
|
skipEnv: false,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(defaultConfig)
|
|
}
|
|
bt := &BaseTable{config: defaultConfig}
|
|
bt.init()
|
|
return bt
|
|
}
|
|
|
|
// init initializes the param table.
|
|
// if refreshInterval greater than 0 will auto refresh config from source
|
|
func (bt *BaseTable) init() {
|
|
formatter := func(key string) string {
|
|
ret := strings.ToLower(key)
|
|
ret = strings.TrimPrefix(ret, "milvus.")
|
|
ret = strings.ReplaceAll(ret, "/", "")
|
|
ret = strings.ReplaceAll(ret, "_", "")
|
|
ret = strings.ReplaceAll(ret, ".", "")
|
|
return ret
|
|
}
|
|
|
|
var err error
|
|
bt.mgr, err = config.Init()
|
|
if err != nil {
|
|
mlog.Error(context.TODO(), "failed to initialize config manager", mlog.Err(err))
|
|
panic(err)
|
|
}
|
|
|
|
if !bt.config.skipEnv {
|
|
err := bt.mgr.AddSource(config.NewEnvSource(formatter))
|
|
if err != nil {
|
|
mlog.Warn(context.TODO(), "init baseTable with env failed", mlog.Err(err))
|
|
return
|
|
}
|
|
}
|
|
bt.initConfigsFromLocal()
|
|
if !bt.config.skipRemote {
|
|
bt.initConfigsFromRemote()
|
|
}
|
|
}
|
|
|
|
func (bt *BaseTable) initConfigsFromLocal() {
|
|
refreshInterval := bt.config.refreshInterval
|
|
var files []string
|
|
for _, file := range bt.config.yamlFiles {
|
|
_, err := os.Stat(path.Join(bt.config.configDir, file))
|
|
// not found
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
mlog.Warn(context.TODO(), "failed to check file", mlog.String("file", file), mlog.Err(err))
|
|
panic(err)
|
|
}
|
|
files = append(files, path.Join(bt.config.configDir, file))
|
|
}
|
|
|
|
err := bt.mgr.AddSource(config.NewFileSource(&config.FileInfo{
|
|
Files: files,
|
|
RefreshInterval: refreshInterval,
|
|
}))
|
|
if err != nil {
|
|
mlog.Warn(context.TODO(), "init baseTable with file failed", mlog.Strings("configFile", bt.config.yamlFiles), mlog.Err(err))
|
|
return
|
|
}
|
|
}
|
|
|
|
func (bt *BaseTable) initConfigsFromRemote() {
|
|
refreshInterval := bt.config.refreshInterval
|
|
etcdConfig := EtcdConfig{}
|
|
etcdConfig.Init(bt)
|
|
etcdConfig.Endpoints.PanicIfEmpty = false
|
|
etcdConfig.RootPath.PanicIfEmpty = false
|
|
if etcdConfig.Endpoints.GetValue() == "" {
|
|
return
|
|
}
|
|
if etcdConfig.UseEmbedEtcd.GetAsBool() && !etcd.HasServer() {
|
|
return
|
|
}
|
|
info := &config.EtcdInfo{
|
|
UseEmbed: etcdConfig.UseEmbedEtcd.GetAsBool(),
|
|
EnableAuth: etcdConfig.EtcdEnableAuth.GetAsBool(),
|
|
UserName: etcdConfig.EtcdAuthUserName.GetValue(),
|
|
PassWord: etcdConfig.EtcdAuthPassword.GetValue(),
|
|
UseSSL: etcdConfig.EtcdUseSSL.GetAsBool(),
|
|
Endpoints: etcdConfig.Endpoints.GetAsStrings(),
|
|
CertFile: etcdConfig.EtcdTLSCert.GetValue(),
|
|
KeyFile: etcdConfig.EtcdTLSKey.GetValue(),
|
|
CaCertFile: etcdConfig.EtcdTLSCACert.GetValue(),
|
|
MinVersion: etcdConfig.EtcdTLSMinVersion.GetValue(),
|
|
KeyPrefix: etcdConfig.RootPath.GetValue(),
|
|
DialTimeout: etcdConfig.DialTimeout.GetAsDuration(time.Millisecond),
|
|
RefreshInterval: refreshInterval,
|
|
}
|
|
|
|
s, err := config.NewEtcdSource(info)
|
|
if err != nil {
|
|
mlog.Info(context.TODO(), "init with etcd failed", mlog.Err(err))
|
|
return
|
|
}
|
|
bt.mgr.AddSource(s)
|
|
s.SetEventHandler(bt.mgr)
|
|
}
|
|
|
|
// GetConfigDir returns the config directory
|
|
func (bt *BaseTable) GetConfigDir() string {
|
|
return bt.config.configDir
|
|
}
|
|
|
|
func initConfPath() string {
|
|
// check if user set conf dir through env
|
|
configDir := os.Getenv("MILVUSCONF")
|
|
if len(configDir) != 0 {
|
|
return configDir
|
|
}
|
|
runPath, err := os.Getwd()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
configDir = runPath + "/configs"
|
|
if _, err := os.Stat(configDir); err != nil {
|
|
_, fpath, _, _ := runtime.Caller(0)
|
|
configDir = path.Dir(fpath) + "/../../../configs"
|
|
}
|
|
return configDir
|
|
}
|
|
|
|
func (bt *BaseTable) FileConfigs() map[string]string {
|
|
return bt.mgr.FileConfigs()
|
|
}
|
|
|
|
func (bt *BaseTable) UpdateSourceOptions(opts ...config.Option) {
|
|
bt.mgr.UpdateSourceOptions(opts...)
|
|
}
|
|
|
|
// Load loads an object with @key.
|
|
func (bt *BaseTable) Load(key string) (string, error) {
|
|
_, v, err := bt.mgr.GetConfig(key)
|
|
return v, err
|
|
}
|
|
|
|
func (bt *BaseTable) Get(key string) string {
|
|
return bt.GetWithDefault(key, "")
|
|
}
|
|
|
|
// GetWithDefault loads an object with @key. If the object does not exist, @defaultValue will be returned.
|
|
func (bt *BaseTable) GetWithDefault(key, defaultValue string) string {
|
|
if bt.mgr == nil {
|
|
return defaultValue
|
|
}
|
|
|
|
_, str, err := bt.mgr.GetConfig(key)
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
return str
|
|
}
|
|
|
|
// Remove Config by key
|
|
func (bt *BaseTable) Remove(key string) error {
|
|
normalizedKey := strings.ToLower(strings.TrimPrefix(key, "milvus."))
|
|
bt.mgr.DeleteConfig(key)
|
|
bt.mgr.EvictCachedValue(key)
|
|
// Fire runtime config update event so watchers (e.g. tracing) can react immediately.
|
|
bt.mgr.OnEvent(&config.Event{
|
|
EventSource: config.RuntimeSource,
|
|
EventType: config.DeleteType,
|
|
Key: normalizedKey,
|
|
Value: "",
|
|
HasUpdated: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// Update Config
|
|
func (bt *BaseTable) Save(key, value string) error {
|
|
normalizedKey := strings.ToLower(strings.TrimPrefix(key, "milvus."))
|
|
bt.mgr.SetConfig(key, value)
|
|
bt.mgr.EvictCachedValue(key)
|
|
// Fire runtime config update event so watchers (e.g. tracing) can react immediately.
|
|
bt.mgr.OnEvent(&config.Event{
|
|
EventSource: config.RuntimeSource,
|
|
EventType: config.UpdateType,
|
|
Key: normalizedKey,
|
|
Value: value,
|
|
HasUpdated: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (bt *BaseTable) SaveGroup(group map[string]string) error {
|
|
for key, value := range group {
|
|
normalizedKey := strings.ToLower(strings.TrimPrefix(key, "milvus."))
|
|
bt.mgr.SetMapConfig(key, value)
|
|
// Fire runtime config update event so watchers (e.g. tracing) can react immediately.
|
|
bt.mgr.OnEvent(&config.Event{
|
|
EventSource: config.RuntimeSource,
|
|
EventType: config.UpdateType,
|
|
Key: normalizedKey,
|
|
Value: value,
|
|
HasUpdated: true,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Reset Config to default value
|
|
func (bt *BaseTable) Reset(key string) error {
|
|
normalizedKey := strings.ToLower(strings.TrimPrefix(key, "milvus."))
|
|
bt.mgr.ResetConfig(key)
|
|
bt.mgr.EvictCachedValue(key)
|
|
// Fire runtime config update event so watchers can refresh their derived state.
|
|
// If the key is not found in any source after reset, emit a DELETE event.
|
|
if _, v, err := bt.mgr.GetConfig(key); err == nil {
|
|
bt.mgr.OnEvent(&config.Event{
|
|
EventSource: config.RuntimeSource,
|
|
EventType: config.UpdateType,
|
|
Key: normalizedKey,
|
|
Value: v,
|
|
HasUpdated: true,
|
|
})
|
|
} else {
|
|
bt.mgr.OnEvent(&config.Event{
|
|
EventSource: config.RuntimeSource,
|
|
EventType: config.DeleteType,
|
|
Key: normalizedKey,
|
|
Value: "",
|
|
HasUpdated: true,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (bt *BaseTable) Manager() *config.Manager {
|
|
return bt.mgr
|
|
}
|