mirror of
https://github.com/zalando/postgres-operator.git
synced 2026-10-04 02:13:59 +02:00
Connection pooler (#799)
Connection pooler support Add support for a connection pooler. The idea is to make it generic enough to be able to switch between different implementations (e.g. pgbouncer or odyssey). Operator needs to create a deployment with pooler and a service for it to access. For connection pool to work properly, a database needs to be prepared by operator, namely a separate user have to be created with an access to an installed lookup function (to fetch credential for other users). This setups is supposed to be used only by robot/application users. Usually a connection pool implementation is more CPU bounded, so it makes sense to create several pods for connection pool with more emphasize on cpu resources. At the moment there are no special affinity or tolerations assigned to bring those pods closer to the database. For availability purposes minimal number of connection pool pods is 2, ideally they have to be distributed between different nodes/AZ, but it's not enforced in the operator itself. Available configuration supposed to be ergonomic and in the normal case require minimum changes to a manifest to enable connection pool. To have more control over the configuration and functionality on the pool side one can customize the corresponding docker image. Co-authored-by: Felix Kunde <felix-kunde@gmx.de>
This commit is contained in:
co-authored by
Felix Kunde
parent
579f78864b
commit
9dfa433363
+181
-1
@@ -23,6 +23,7 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
oldSpec := c.Postgresql
|
||||
c.setSpec(newSpec)
|
||||
|
||||
defer func() {
|
||||
@@ -108,6 +109,11 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
|
||||
}
|
||||
}
|
||||
|
||||
// sync connection pool
|
||||
if err = c.syncConnectionPool(&oldSpec, newSpec, c.installLookupFunction); err != nil {
|
||||
return fmt.Errorf("could not sync connection pool: %v", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -424,7 +430,9 @@ func (c *Cluster) syncSecrets() error {
|
||||
}
|
||||
pwdUser := userMap[secretUsername]
|
||||
// if this secret belongs to the infrastructure role and the password has changed - replace it in the secret
|
||||
if pwdUser.Password != string(secret.Data["password"]) && pwdUser.Origin == spec.RoleOriginInfrastructure {
|
||||
if pwdUser.Password != string(secret.Data["password"]) &&
|
||||
pwdUser.Origin == spec.RoleOriginInfrastructure {
|
||||
|
||||
c.logger.Debugf("updating the secret %q from the infrastructure roles", secretSpec.Name)
|
||||
if _, err = c.KubeClient.Secrets(secretSpec.Namespace).Update(secretSpec); err != nil {
|
||||
return fmt.Errorf("could not update infrastructure role secret for role %q: %v", secretUsername, err)
|
||||
@@ -468,6 +476,16 @@ func (c *Cluster) syncRoles() (err error) {
|
||||
for _, u := range c.pgUsers {
|
||||
userNames = append(userNames, u.Name)
|
||||
}
|
||||
|
||||
if c.needConnectionPool() {
|
||||
connPoolUser := c.systemUsers[constants.ConnectionPoolUserKeyName]
|
||||
userNames = append(userNames, connPoolUser.Name)
|
||||
|
||||
if _, exists := c.pgUsers[connPoolUser.Name]; !exists {
|
||||
c.pgUsers[connPoolUser.Name] = connPoolUser
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers, err = c.readPgUsersFromDatabase(userNames)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting users from the database: %v", err)
|
||||
@@ -600,3 +618,165 @@ func (c *Cluster) syncLogicalBackupJob() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cluster) syncConnectionPool(oldSpec, newSpec *acidv1.Postgresql, lookup InstallFunction) error {
|
||||
if c.ConnectionPool == nil {
|
||||
c.ConnectionPool = &ConnectionPoolObjects{}
|
||||
}
|
||||
|
||||
newNeedConnPool := c.needConnectionPoolWorker(&newSpec.Spec)
|
||||
oldNeedConnPool := c.needConnectionPoolWorker(&oldSpec.Spec)
|
||||
|
||||
if newNeedConnPool {
|
||||
// Try to sync in any case. If we didn't needed connection pool before,
|
||||
// it means we want to create it. If it was already present, still sync
|
||||
// since it could happen that there is no difference in specs, and all
|
||||
// the resources are remembered, but the deployment was manualy deleted
|
||||
// in between
|
||||
c.logger.Debug("syncing connection pool")
|
||||
|
||||
// in this case also do not forget to install lookup function as for
|
||||
// creating cluster
|
||||
if !oldNeedConnPool || !c.ConnectionPool.LookupFunction {
|
||||
newConnPool := newSpec.Spec.ConnectionPool
|
||||
|
||||
specSchema := ""
|
||||
specUser := ""
|
||||
|
||||
if newConnPool != nil {
|
||||
specSchema = newConnPool.Schema
|
||||
specUser = newConnPool.User
|
||||
}
|
||||
|
||||
schema := util.Coalesce(
|
||||
specSchema,
|
||||
c.OpConfig.ConnectionPool.Schema)
|
||||
|
||||
user := util.Coalesce(
|
||||
specUser,
|
||||
c.OpConfig.ConnectionPool.User)
|
||||
|
||||
if err := lookup(schema, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.syncConnectionPoolWorker(oldSpec, newSpec); err != nil {
|
||||
c.logger.Errorf("could not sync connection pool: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if oldNeedConnPool && !newNeedConnPool {
|
||||
// delete and cleanup resources
|
||||
if err := c.deleteConnectionPool(); err != nil {
|
||||
c.logger.Warningf("could not remove connection pool: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !oldNeedConnPool && !newNeedConnPool {
|
||||
// delete and cleanup resources if not empty
|
||||
if c.ConnectionPool != nil &&
|
||||
(c.ConnectionPool.Deployment != nil ||
|
||||
c.ConnectionPool.Service != nil) {
|
||||
|
||||
if err := c.deleteConnectionPool(); err != nil {
|
||||
c.logger.Warningf("could not remove connection pool: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Synchronize connection pool resources. Effectively we're interested only in
|
||||
// synchronizing the corresponding deployment, but in case of deployment or
|
||||
// service is missing, create it. After checking, also remember an object for
|
||||
// the future references.
|
||||
func (c *Cluster) syncConnectionPoolWorker(oldSpec, newSpec *acidv1.Postgresql) error {
|
||||
deployment, err := c.KubeClient.
|
||||
Deployments(c.Namespace).
|
||||
Get(c.connPoolName(), metav1.GetOptions{})
|
||||
|
||||
if err != nil && k8sutil.ResourceNotFound(err) {
|
||||
msg := "Deployment %s for connection pool synchronization is not found, create it"
|
||||
c.logger.Warningf(msg, c.connPoolName())
|
||||
|
||||
deploymentSpec, err := c.generateConnPoolDeployment(&newSpec.Spec)
|
||||
if err != nil {
|
||||
msg = "could not generate deployment for connection pool: %v"
|
||||
return fmt.Errorf(msg, err)
|
||||
}
|
||||
|
||||
deployment, err := c.KubeClient.
|
||||
Deployments(deploymentSpec.Namespace).
|
||||
Create(deploymentSpec)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.ConnectionPool.Deployment = deployment
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("could not get connection pool deployment to sync: %v", err)
|
||||
} else {
|
||||
c.ConnectionPool.Deployment = deployment
|
||||
|
||||
// actual synchronization
|
||||
oldConnPool := oldSpec.Spec.ConnectionPool
|
||||
newConnPool := newSpec.Spec.ConnectionPool
|
||||
specSync, specReason := c.needSyncConnPoolSpecs(oldConnPool, newConnPool)
|
||||
defaultsSync, defaultsReason := c.needSyncConnPoolDefaults(newConnPool, deployment)
|
||||
reason := append(specReason, defaultsReason...)
|
||||
if specSync || defaultsSync {
|
||||
c.logger.Infof("Update connection pool deployment %s, reason: %+v",
|
||||
c.connPoolName(), reason)
|
||||
|
||||
newDeploymentSpec, err := c.generateConnPoolDeployment(&newSpec.Spec)
|
||||
if err != nil {
|
||||
msg := "could not generate deployment for connection pool: %v"
|
||||
return fmt.Errorf(msg, err)
|
||||
}
|
||||
|
||||
oldDeploymentSpec := c.ConnectionPool.Deployment
|
||||
|
||||
deployment, err := c.updateConnPoolDeployment(
|
||||
oldDeploymentSpec,
|
||||
newDeploymentSpec)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.ConnectionPool.Deployment = deployment
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
service, err := c.KubeClient.
|
||||
Services(c.Namespace).
|
||||
Get(c.connPoolName(), metav1.GetOptions{})
|
||||
|
||||
if err != nil && k8sutil.ResourceNotFound(err) {
|
||||
msg := "Service %s for connection pool synchronization is not found, create it"
|
||||
c.logger.Warningf(msg, c.connPoolName())
|
||||
|
||||
serviceSpec := c.generateConnPoolService(&newSpec.Spec)
|
||||
service, err := c.KubeClient.
|
||||
Services(serviceSpec.Namespace).
|
||||
Create(serviceSpec)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.ConnectionPool.Service = service
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("could not get connection pool service to sync: %v", err)
|
||||
} else {
|
||||
// Service updates are not supported and probably not that useful anyway
|
||||
c.ConnectionPool.Service = service
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user