sonic messaging mechanism and architecture

Posted by ashwin on Sat, 27 Jul 2019 11:15:18 +0200

Sonic is a network operating system, using a large number of independent third-party open source components, which depend on, compile environment, libraries, configuration methods are very different. In order to make these components cooperate with each other in sonic and not interfere with each other, and at the same time try not to modify the code of third-party components, sonic uses container technology to provide independent running environment for each component, and communicates through shared network namespace between containers.

Each third component has its own configuration file format and message format. How to make these components communicate with each other? sonic uses redis database as a messaging platform, shielding plug-ins of each component through pure character messaging, and glue code to glue them together.

sonic message framework diagram

Realization

sonic implements the whole messaging mechanism through publish-subscribe mechanism and key space event mechanism of redis database.

base class

class TableBase {
public:
    TableBase(int dbId, const std::string &tableName)
        : m_tableName(tableName), m_dbId(dbId)
    {
        /* Look up table separator for the provided DB */
        auto it = tableNameSeparatorMap.find(dbId);

        if (it != tableNameSeparatorMap.end())
        {
            m_tableSeparator = it->second;
        }
        else
        {
            SWSS_LOG_NOTICE("Unrecognized database ID. Using default table name separator ('%s')", TABLE_NAME_SEPARATOR_VBAR.c_str());
            m_tableSeparator = TABLE_NAME_SEPARATOR_VBAR;
        }
    }

    std::string getTableName() const { return m_tableName; }
    int getDbId() const { return m_dbId; }

    /* Return the actual key name as a combination of tableName<table_separator>key */
    std::string getKeyName(const std::string &key)
    {
        if (key == "") return m_tableName;
        else return m_tableName + m_tableSeparator + key;
    }

    /* Return the table name separator being used */
    std::string getTableNameSeparator() const
    {
        return m_tableSeparator;
    }

    std::string getChannelName() { return m_tableName + "_CHANNEL"; }
private:
    static const std::string TABLE_NAME_SEPARATOR_COLON;
    static const std::string TABLE_NAME_SEPARATOR_VBAR;
    static const TableNameSeparatorMap tableNameSeparatorMap;

    std::string m_tableName;
    std::string m_tableSeparator;
    int m_dbId;
};
class TableEntryWritable {
public:
    virtual ~TableEntryWritable() = default;

    /* Set an entry in the table */
    virtual void set(const std::string &key,
                     const std::vector<FieldValueTuple> &values,
                     const std::string &op = "",
                     const std::string &prefix = EMPTY_PREFIX) = 0;
    /* Delete an entry in the table */
    virtual void del(const std::string &key,
                     const std::string &op = "",
                     const std::string &prefix = EMPTY_PREFIX) = 0;

};

Consumer base class

 Consumers can respond to producer events by blocking or polling. sonic uses the asynchronous event notification mechanism (poll) for processing. Consumer classes must implement interfaces related to event notification mechanisms.
RedisSelect

This class encapsulates the asynchronous notification mechanism Selectable(select, poll, etc.). A derived class integrated with this class can join the asynchronous event mechanism. By integrating this class, consumers can continuously monitor events.

class RedisSelect : public Selectable
{
public:
    /* The database is already alive and kicking, no need for more than a second */
    static constexpr unsigned int SUBSCRIBE_TIMEOUT = 1000;

    RedisSelect(int pri = 0);//Scheduling priority

    int getFd() override;
    void readData() override;
    bool hasCachedData() override;
    bool initializedWithData() override;
    void updateAfterRead() override;

    /* Create a new redisContext, SELECT DB and SUBSCRIBE */
    void subscribe(DBConnector* db, const std::string &channelName);

    /* PSUBSCRIBE */
    void psubscribe(DBConnector* db, const std::string &channelName);

    void setQueueLength(long long int queueLength);

protected:
    std::unique_ptr<DBConnector> m_subscribe;
    long long int m_queueLength;//The number of replies received, one request and one response.
};
getFd
int RedisSelect::getFd()
{
    return m_subscribe->getContext()->fd;
}
readData
void RedisSelect::readData()
{
    redisReply *reply = nullptr;

    if (redisGetReply(m_subscribe->getContext(), reinterpret_cast<void**>(&reply)) != REDIS_OK)
        throw std::runtime_error("Unable to read redis reply");

    freeReplyObject(reply);
    m_queueLength++;//Incident plus one,

    reply = nullptr;
    int status;
    do
    {
        status = redisGetReplyFromReader(m_subscribe->getContext(), reinterpret_cast<void**>(&reply));
        if(reply != nullptr && status == REDIS_OK)
        {//If a response is added once, the value will be greater than the number of cycles that are finally processed, resulting in idling, but if not, in extreme cases, information loss will occur.
            m_queueLength++;
            freeReplyObject(reply);
        }
    }
    while(reply != nullptr && status == REDIS_OK);

    if (status != REDIS_OK)
    {
        throw std::runtime_error("Unable to read redis reply");
    }
}
hasCachedData
bool RedisSelect::hasCachedData()
{//To determine whether there is a message, add m_ready if there is a message to ensure that the read message can be processed.
    return m_queueLength > 1;
}
updateAfterRead
void RedisSelect::updateAfterRead()
{
    m_queueLength--;//Suppose you process one response at a time and subtract 1 from it. Even if you process more than one message at a time, you still subtract only 1, which is the root cause of idling.
}
setQueueLength
void RedisSelect::setQueueLength(long long int queueLength)
{
    m_queueLength = queueLength;//Set the number of messages used for constructors
}
subscribe and psubscribe
/* Create a new redisContext, SELECT DB and SUBSCRIBE */
void RedisSelect::subscribe(DBConnector* db, const std::string &channelName)
{
    m_subscribe.reset(db->newConnector(SUBSCRIBE_TIMEOUT));

    /* Send SUBSCRIBE #channel command */
    std::string s("SUBSCRIBE ");
    s += channelName;
    RedisReply r(m_subscribe.get(), s, REDIS_REPLY_ARRAY);
}

/* PSUBSCRIBE */
void RedisSelect::psubscribe(DBConnector* db, const std::string &channelName)
{
    m_subscribe.reset(db->newConnector(SUBSCRIBE_TIMEOUT));

    /*
     * Send PSUBSCRIBE #channel command on the
     * non-blocking subscriber DBConnector
     */
    std::string s("PSUBSCRIBE ");
    s += channelName;
    RedisReply r(m_subscribe.get(), s, REDIS_REPLY_ARRAY);
}

Consumers further encapsulate base classes

class TableEntryPoppable {
public:
    virtual ~TableEntryPoppable() = default;

    /* Pop an action (set or del) on the table */
    virtual void pop(KeyOpFieldsValuesTuple &kco, const std::string &prefix = EMPTY_PREFIX) = 0;

    /* Get multiple pop elements */
    virtual void pops(std::deque<KeyOpFieldsValuesTuple> &vkco, const std::string &prefix = EMPTY_PREFIX) = 0;
};

class TableConsumable : public TableBase, public TableEntryPoppable, public RedisSelect {
public:
    /* The default value of pop batch size is 128 */
    static constexpr int DEFAULT_POP_BATCH_SIZE = 128;//One-time consumption 128 news items

    TableConsumable(int dbId, const std::string &tableName, int pri) : TableBase(dbId, tableName), RedisSelect(pri) { }
};

redis lua execution script

EVAL script numkeys key [key ...] arg [arg ...]
First of all, you must know the grammatical format of eval, in which:
   Script: your lua script
   <2> Numkeys: Number of keys
   <3> key: alternative symbols for various data structures in redis
   <4> arg: Your custom parameters
 ok, maybe at first glance the template is not very clear. Now I can use a small example of the official website to demonstrate:
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 username age jack 20

What does the above code mean? The first parameter string is script, or lua script. 2 represents the number of keys, KEYS[1] is the placeholder of username, KEYS[2] is the placeholder of age, ARGV[1] is the placeholder of jack, ARGV[2] is the placeholder of 20, and so on, so the final result should be: {return username age jack 20} is a bit like the placeholder in C: {0}? Next I'll show you in Redis:

admin@admin:~$ redis-cli
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 username age jack 20
1) "username"
2) "age"
3) "jack"
4) "20"
127.0.0.1:6379>

Then we execute it with the following command, which is different from the previous one. The parameters -- Eval script key1 key2, arg1 age2, key and value are separated by a comma. Finally, we see that the data is out, right.

admin@admin:~$  redis-cli --eval t.lua username age , jack 20                 
1) "username"
2) "age"
3) "jack"
4) "20"
admin@admin:~$
Notice that there are spaces on both sides of the comma above.
  • The script can also be executed in REPL mode, but the script needs to be loaded first:
admin@admin:~$ redis-cli script load "$(cat t.lua)"                          
"a42059b356c875f0717db19a51f6aaca9ae659ea"
admin@admin:~$
admin@admin:~$ redis-cli
127.0.0.1:6379> EVALSHA a42059b356c875f0717db19a51f6aaca9ae659ea 2 username age jack 20
1) "username"
2) "age"
3) "jack"
4) "20"
127.0.0.1:6379>
  • lua scripts compare sizes, using the function tonumber to convert characters into numbers, and then compare sizes
admin@admin:~$ cat flashsale.lua
local buyNum = ARGV[1]                        -- Quantity of this purchase
local goodsKey = KEYS[1]                      -- Name of the goods purchased
local goodsNum = redis.call('get',goodsKey)   -- Number of Inventories of Goods Obtained
if tonumber(goodsNum) >= tonumber(buyNum)     -- Inventory is sufficient, then shipment
    then redis.call('decrby',goodsKey,buyNum) -- Reduce the amount of this purchase
    return buyNum                             -- Return the amount purchased
else
    return '0'                                -- Not enough, return directly to 0
end
admin@admin:~$
admin@admin:~$ redis-cli --eval flashsale.lua "pets" , 8
"8"
admin@admin:~$
//The script above implements a simple secondkill program

Topics: C++ Redis Database network