Beispiel #1
0
async def createCluster(args):
    '''
    See clusterManagerCommandCreate in redis-cli.c

    The main difference is that we first set slots
    Then run cluster meet
    Then set replicas, as setting replicas requires the master to be known by the
    replica, and hence the meet to have happened already

    Another difference is master/replica assignment from the input list of ip address
    that could be changed easily to match redis-cli behavior.

    Tested with redis-server 4.x. In redis-4 time there was still redis-trib and no
    redis-cli, so we cannot use redis-cli to initialize the cluster
    '''

    logging.info(args)

    nodes = args.ips.split()

    #
    # Reset all first
    #
    for nodeAddress in nodes:
        redisUrl = f'redis://{nodeAddress}'
        client = RedisClient(redisUrl, args.password, args.user)

        dbSize = await client.send('DBSIZE')
        if dbSize > 0:
            try:
                await client.send('FLUSHALL')
            except Exception as e:
                logging.warning(f'Ignoring error with FLUSHALL: {e}')

        await client.send('CLUSTER', 'RESET', 'hard')

    #
    # Hash slot allocation
    #
    click.secho(
        f'>>> Performing hash slots allocation on {args.shards} nodes...',
        bold=True)

    slotsPerNode = SLOTS // args.shards
    offset = 0
    allSlots = []

    for i in range(args.shards):
        nodeSlots = []
        for j in range(slotsPerNode):
            nodeSlots.append(offset + j)

        offset += slotsPerNode
        allSlots.append(nodeSlots)

    # reminder
    for j in range(SLOTS % args.shards):
        allSlots[-1].append(offset + j)

    # Print slot assignment
    for i, nodeSlots in enumerate(allSlots):
        # Master[0] -> Slots 0 - 4095
        print('Master[{}] -> Slots {} - {}'.format(i, nodeSlots[0],
                                                   nodeSlots[-1]))

    # Setting slots
    for i in range(args.shards):
        nodeAddress = nodes[i]
        redisUrl = f'redis://{nodeAddress}'
        masterClient = RedisClient(redisUrl, args.password, args.user)
        await masterClient.send('CLUSTER', 'ADDSLOTS', *allSlots[i])

    # Give each node its own 'epoch'
    click.secho('>>> Assign a different config epoch to each node', bold=True)
    for i, nodeAddress in enumerate(nodes):
        epoch = i + 1
        redisUrl = f'redis://{nodeAddress}'
        client = RedisClient(redisUrl, args.password, args.user)

        try:
            await client.send('CLUSTER', 'SET-CONFIG-EPOCH', epoch)
        except Exception as e:
            logging.warning(f'{redisUrl}: Error with set-config-epoch: {e}')
            pass

    click.secho('>>> Sending CLUSTER MEET messages to join the cluster',
                bold=True)
    firstNodeIp, _, firstNodePort = nodes[0].partition(':')

    for nodeAddress in nodes[1:]:
        redisUrl = f'redis://{nodeAddress}'
        client = RedisClient(redisUrl, args.password, args.user)
        await client.send('CLUSTER', 'MEET', firstNodeIp, firstNodePort)

    # Wait one second
    await asyncio.sleep(1)

    # Wait for all the nodes in the cluster to agree on node count
    # so that they all know about each other, and we can set replicas
    # Give us 15 seconds max to do that
    start = time.time()
    timeout = 15

    for nodeAddress in nodes:
        redisUrl = f'redis://{nodeAddress}'
        nodeCount = await getClusterNodesCount(redisUrl, args.password,
                                               args.user)
        if nodeCount != args.shards:
            await asyncio.sleep(0.1)
            sys.stderr.write('.')
            sys.stderr.flush()

            if time.time() - start > timeout:
                raise ValueError(
                    'Timed out while waiting for all nodes to see each other')

    sys.stderr.write('\n')

    #
    # [m1, m2, ..., mn, r1, r2, ...]
    # [m1, m2, ...,      mn, r1, r2, ..., rn,    s1, s2, ...,    sn]
    #
    # <----- args.shards -> <--- args.shards --> <--- args.shards ->
    #
    # r1 = args.shards + 1
    # ...
    # ri = args.shards + i
    #
    # s1 = replicas * args.shards + 1
    # ...
    # si = replicas * args.shards + i
    #

    # Now we can set the replicas
    click.secho('>>> Assigning replicas', bold=True)
    for i in range(args.shards):
        masterNodeAddress = nodes[i]
        redisUrl = f'redis://{masterNodeAddress}'
        masterClient = RedisClient(redisUrl, args.password, args.user)
        masterNodeId = await masterClient.send('CLUSTER', 'MYID')

        for j in range(args.replicas):
            replicaIp = nodes[(j + 1) * args.shards + i]
            print(f'Setting replica {replicaIp} to {masterNodeAddress}')

            url = f'redis://{replicaIp}'
            replicaClient = RedisClient(url, args.password, args.user)

            await replicaClient.send('CLUSTER', 'REPLICATE', masterNodeId)
Beispiel #2
0
def makeClientfromNode(node, redisPassword):
    url = f'redis://{node.ip}:{node.port}'
    return RedisClient(url, redisPassword)
Beispiel #3
0
async def analyzeKeyspace(
    redisUrlsStr: str,
    redisPassword: str,
    redisUser: str,
    timeout: int,
    count: int = -1,
    monitor: bool = False,
):
    pattern = '__key*__:*'

    redisUrls = redisUrlsStr.split(';')
    redisUrl = redisUrls[0]

    redisClient = RedisClient(redisUrl, redisPassword, redisUser)
    await redisClient.connect()

    clients = []
    if redisClient.cluster:
        nodes = await redisClient.cluster_nodes()
        masterNodes = [node for node in nodes if node.role == 'master']
        for node in masterNodes:
            client = makeClientfromNode(node, redisPassword, redisUser)
            clients.append(client)
    else:
        clients = [RedisClient(url, redisPassword, redisUser) for url in redisUrls]

    #
    # E for Keyevent events, published with __keyevent@<db>__ prefix.
    # A Alias for g$lshztxe, to catch all events
    #
    keyspaceConfig = 'AE'

    async def pubSubCallback(client, obj, message):
        '''Need to extract a key and a command from the pubsub payload'''

        msg = message[2].decode()
        _, _, cmd = msg.partition(':')
        cmd = cmd.upper()

        key = message[3].decode()
        obj.keys[key] += 1
        obj.notifications += 1

        node = f'{client.host}:{client.port}'
        obj.nodes[node] += 1
        obj.commands[cmd] += 1

    async def monitorCallback(client, obj, message):
        '''Need to extract a key and a command from the monitor payload'''

        #
        # We need to skip the beginning of such lines
        # 1586739509.775473 [0 [::1]:54588] "XADD" "58c52262_channel_99" "MAXLEN" ...
        #
        line = message.decode()

        # FIXME / parsing is not robust, if there are spaces in keys
        tokens = line.split()
        tokens = tokens[3:]

        cmd = tokens[0][1:-1]  # we need to remove the double quotes from key and cmd
        cmd = cmd.upper()

        # Some keys are located in odd places
        if len(tokens) > 1:
            key = tokens[1][1:-1]

            if cmd in ('XREAD', 'XREADGROUP'):
                try:
                    idx = tokens.index('"STREAMS"') + 1
                except ValueError:
                    raise ValueError(
                        "{0} arguments do not contain STREAMS operand".format(cmd)
                    )
                key = tokens[idx]
            elif cmd in ('XGROUP', 'XINFO'):
                key = tokens[2]
            else:
                key = tokens[1]

            key = key[1:-1]
            obj.keys[key] += 1

        # We need key and command
        obj.notifications += 1

        node = f'{client.host}:{client.port}'
        obj.nodes[node] += 1
        obj.commands[cmd] += 1

    tasks = []

    keySpace = KeySpace()

    # First we need to make sure keyspace notifications are ON
    # Do this manually with redis-cli -p 10000 config set notify-keyspace-events KEAt
    if not monitor:
        confs = []
        for client in clients:
            conf = await client.send('CONFIG', 'GET', 'notify-keyspace-events')
            if conf[1]:
                print(f'{client} current keyspace config: {conf[1].decode()}')
                confs.append(conf[1].decode())

            # Set the new conf
            await client.send('CONFIG', 'SET', 'notify-keyspace-events', keyspaceConfig)

    try:
        for client in clients:
            if monitor:
                task = asyncio.ensure_future(client.monitor(monitorCallback, keySpace))
            else:
                task = asyncio.ensure_future(
                    client.psubscribe(pattern, pubSubCallback, keySpace)
                )
            tasks.append(task)

        if count > 0:
            label = f'Capturing {count} events'
            with click.progressbar(length=count, label=label) as bar:

                progress = 0
                while keySpace.notifications < count:
                    await asyncio.sleep(0.1)
                    bar.update(keySpace.notifications - progress)
                    progress = keySpace.notifications
        else:
            label = f'Capturing events during {timeout} seconds'
            with click.progressbar(length=timeout, label=label) as bar:
                # Monitor during X seconds
                for i in range(timeout):
                    await asyncio.sleep(1)
                    bar.update(1)

        for task in tasks:
            # Cancel the tasks
            task.cancel()
            await task

    finally:
        if not monitor:
            # Now restore the notification
            for client, conf in zip(clients, confs):
                # reset the previous conf
                print(f'resetting old config {conf}')
                await client.send('CONFIG', 'SET', 'notify-keyspace-events', conf)

    keySpace.describe()

    return keySpace
Beispiel #4
0
async def binPackingReshardCoroutine(
    redisUrls, redisPassword, weights, timeout, dry=False, nodeId=None
):
    redisClient = RedisClient(redisUrls, redisPassword)
    nodes = await redisClient.cluster_nodes()

    # There will be as many bins as there are master nodes
    masterNodes = [node for node in nodes if node.role == 'master']
    masterClients = [makeClientfromNode(node, redisPassword) for node in masterNodes]
    binCount = len(masterNodes)

    # Multiple keys could hash to the same hash-slot (collisions), so
    # we need to feed to binpacking a list of [slot: weight], not
    # a list of [key: weight]
    hashSlotWeights = collections.defaultdict(int)
    for key, weight in weights.items():
        slot = getHashSlot(key)
        hashSlotWeights[slot] += weight

    # Run the bin packing algorithm
    bins = to_constant_bin_number(hashSlotWeights, binCount)

    # A list of list of slots to migrate, for each node
    allSlots = []

    for b in bins:
        # b is a dictionary of [name, weight] / name is the hash slot.
        # we want to sort by hash slot to make this process deterministic
        binSlots = [slot for slot in b.keys()]
        binSlots.sort()
        allSlots.append(binSlots)

    # We need to know where each slots lives
    slotToNodes = await getSlotsToNodesMapping(redisUrls, redisPassword)

    totalMigratedSlots = 0

    for binSlots, node in zip(allSlots, masterNodes):

        print(f'== {node.node_id} / {node.ip}:{node.port} ==')
        migratedSlots = 0

        if nodeId is not None and node.node_id != nodeId:
            continue

        for slot in binSlots:
            sourceNode = slotToNodes[slot]
            logging.debug(f'{slot} owned by {sourceNode.node_id}')

        # Migrate each slot
        for slot in binSlots:
            # recompute the slots to node mapping after each node migration
            slotToNodes = await getSlotsToNodesMapping(redisUrls, redisPassword)

            sourceNode = slotToNodes[slot]
            if sourceNode.node_id != node.node_id:

                ret = await migrateSlot(
                    masterClients, redisPassword, slot, sourceNode, node, dry
                )
                if not ret:
                    return False

                migratedSlots += 1

        print(f'migrated {migratedSlots} slots')
        totalMigratedSlots += migratedSlots

        #
        # This section is key.
        # We periodically make sure that all nodes in the cluster agree on their view
        # of the cluster, mostly on how slots are allocated
        #
        # Without this wait, if we try to keep on moving other slots
        # the cluster will become broken,
        # and commands such as redis-cli --cluste check will report it as inconsistent
        #
        # note that existing redis cli command do not migrate to multiple nodes at once
        # while this script does
        #
        consistent = await waitForClusterViewToBeConsistent(
            redisUrls, redisPassword, timeout
        )
        if not consistent:
            return False

    print(f'total migrated slots: {migratedSlots}')
    return True
Beispiel #5
0
async def redisSubscriber(
    client: RedisClient,
    stream: str,
    position: Optional[str],
    messageHandlerClass: RedisSubscriberMessageHandlerClass,  # noqa
    obj,
):
    messageHandler = messageHandlerClass(obj)

    logPrefix = f'subscriber[{stream}]: {client}'

    streamExists = False
    redisHost = client.host
    clientId = -1

    if client:
        # query the stream size
        try:
            streamExists = await client.send('EXISTS', stream)
            clientId = await client.send('CLIENT', 'ID', key=stream)
            redisHost = await getHostForKey(client, stream)
        except Exception as e:
            logging.error(f"{logPrefix} cannot retreive stream metadata: {e}")
            client = None

    initInfo = {
        'success': client is not None,
        'redis_node': redisHost,
        'redis_client_id': clientId,
        'stream_exists': streamExists,
        'stream_name': stream,
    }

    try:
        await messageHandler.on_init(initInfo)
    except Exception as e:
        logging.error(f'{logPrefix} cannot initialize message handler: {e}')
        client = None

    if client is None:
        return messageHandler

    # lastId = '0-0'
    lastId = '$' if position is None else position

    try:
        # wait for incoming events.
        while True:
            results = await client.send('XREAD', 'BLOCK', b'0', b'STREAMS',
                                        stream, lastId)

            results = results[stream.encode()]

            for result in results:
                lastId = result[0].decode()
                msg = result[1]
                data = msg[b'json']

                msgCksum = msg.get(b'sha1')
                if msgCksum is not None:
                    cksum = sha1(data).hexdigest().encode()
                    if cksum != msgCksum:
                        err = f'{lastId}: invalid xread msg cksum'
                        logging.error(err)
                        continue

                payloadSize = len(data)
                try:
                    msg = json.loads(data)
                except json.JSONDecodeError:
                    msgEncoded = base64.b64encode(data).decode()
                    err = f'{lastId}: malformed json: base64: {msgEncoded} raw: {data}'
                    logging.error(err)
                    continue

                ret = await messageHandler.handleMsg(msg, lastId, payloadSize)
                if not ret:
                    break

    except asyncio.CancelledError:
        messageHandler.log('Cancelling redis subscription')
        raise

    except Exception as e:
        messageHandler.log(e)
        backtrace = traceback.format_exc()
        messageHandler.log(
            f'{logPrefix} Generic Exception caught in {backtrace}')

    finally:
        messageHandler.log('Closing redis subscription')

        # When finished, close the connection.
        client.close()

        return messageHandler